paperlint 3.2.0 → 4.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 +2 -2
- package/dist/adapters/banal/failure.d.ts +1 -7
- package/dist/adapters/banal/failure.d.ts.map +1 -1
- package/dist/adapters/banal/failure.js +5 -7
- package/dist/adapters/banal/failure.js.map +1 -1
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +8 -11
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/install.d.ts.map +1 -1
- package/dist/adapters/banal/install.js +4 -3
- package/dist/adapters/banal/install.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +9 -4
- package/dist/adapters/banal/invocation.d.ts.map +1 -1
- package/dist/adapters/banal/invocation.js +13 -1
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/locate.d.ts +12 -9
- package/dist/adapters/banal/locate.d.ts.map +1 -1
- package/dist/adapters/banal/locate.js +19 -12
- package/dist/adapters/banal/locate.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +10 -1
- package/dist/adapters/banal/run.d.ts.map +1 -1
- package/dist/adapters/banal/run.js +8 -4
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +1 -1
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +2 -1
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +8 -2
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +6 -3
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js.map +1 -1
- package/dist/adapters/node/workspace.io.d.ts.map +1 -1
- package/dist/adapters/node/workspace.io.js +4 -2
- package/dist/adapters/node/workspace.io.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +4 -2
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +8 -1
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +2 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -6
- package/dist/build.js.map +1 -1
- package/dist/caller-path.d.ts +3 -0
- package/dist/caller-path.d.ts.map +1 -0
- package/dist/caller-path.js +13 -0
- package/dist/caller-path.js.map +1 -0
- package/dist/cli.d.ts +10 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +62 -12
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +12 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/domain/paths.d.ts +8 -2
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +12 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/ready.d.ts +24 -0
- package/dist/domain/ready.d.ts.map +1 -0
- package/dist/domain/ready.js +11 -0
- package/dist/domain/ready.js.map +1 -0
- package/dist/domain/record.d.ts +8 -0
- package/dist/domain/record.d.ts.map +1 -0
- package/dist/domain/record.js +8 -0
- package/dist/domain/record.js.map +1 -0
- package/dist/domain/sha256.d.ts.map +1 -1
- package/dist/domain/sha256.js +4 -2
- package/dist/domain/sha256.js.map +1 -1
- package/dist/engine.d.ts +23 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
- package/dist/eslint-rules/install-path-literals.d.ts +2 -2
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/paper-research-question.d.ts +1 -1
- package/dist/eslint-rules/paper-research-question.js.map +1 -1
- package/dist/eslint-rules/paper-stages.d.ts +2 -2
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +6 -6
- 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.map +1 -1
- package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
- package/dist/eslint-rules/review-frontmatter.js.map +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
- package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts +2 -2
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +29 -26
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -2
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +3 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +18 -8
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +1 -5
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +5 -1
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -6
- package/dist/paper-files.js.map +1 -1
- package/dist/paper-settings.js +3 -3
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts +51 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +23 -15
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +2 -3
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/tool-installer.d.ts +2 -14
- package/dist/ports/tool-installer.d.ts.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +16 -4
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +4 -4
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts +26 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +9 -9
- package/dist/rules-config.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 +14 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +20 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +2 -2
- package/eslint-rules/bib-reachable-entry.ts +1 -1
- package/eslint-rules/install-path-literals.ts +2 -2
- package/eslint-rules/paper-research-question.ts +1 -1
- package/eslint-rules/paper-stages.ts +2 -2
- package/eslint-rules/paper-typography.ts +2 -2
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/review-frontmatter.ts +1 -1
- package/eslint-rules/temp-root-realpath.ts +1 -1
- package/eslint-rules/tex-build.ts +2 -2
- package/eslint.config.mjs +7 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
- package/fixtures/install-path-literals/clean.md +2 -2
- package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
- package/fixtures/install-path-literals/defect.md +2 -2
- package/hooks/paper-status-gates.sh +9 -19
- package/lib/paper-config.harness.mjs +1 -1
- package/lib/paper-config.ts +1 -5
- package/lib/skill-checks.mjs +24 -381
- package/lib/skill-checks.test.mjs +63 -115
- package/lib/skill-corpus.mjs +10 -251
- package/lib/skill-corpus.test.mjs +2 -13
- package/lib/skill-trigger-cases.mjs +1 -1
- package/package.json +1 -1
- package/scripts/coverage-config.test.ts +0 -1
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/analyze-sibling-paper/SKILL.md +1 -39
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
- package/skills/argument-arc/SKILL.md +2 -36
- package/skills/argument-arc/SKILL.md.spec.ts +1 -43
- package/skills/build-benchmark/SKILL.md +1 -41
- package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
- package/skills/camera-ready/SKILL.md +1 -34
- package/skills/camera-ready/SKILL.md.spec.ts +0 -33
- package/skills/cold-read-diff/SKILL.md +1 -35
- package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
- package/skills/draft-paper/SKILL.md +2 -41
- package/skills/draft-paper/SKILL.md.spec.ts +1 -50
- package/skills/extend-paper/SKILL.md +2 -33
- package/skills/extend-paper/SKILL.md.spec.ts +1 -42
- package/skills/find-venue/SKILL.md +2 -38
- package/skills/find-venue/SKILL.md.spec.ts +1 -47
- package/skills/grade-paper-writing/SKILL.md +3 -38
- package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
- package/skills/grade-paper-writing/prose-lint.mjs +2 -3
- package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
- package/skills/harden-paper/SKILL.md +1 -39
- package/skills/harden-paper/SKILL.md.spec.ts +0 -38
- package/skills/map-prior-work/SKILL.md +1 -35
- package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
- package/skills/paper-adversarial-review/SKILL.md +2 -35
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
- package/skills/paper-pipeline/SKILL.md +2 -28
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
- package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
- package/skills/paper-pipeline/scripts/README.md +30 -225
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
- package/skills/paper-pipeline/scripts/consumer.mjs +23 -104
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -42
- package/skills/paper-status/SKILL.md.spec.ts +1 -41
- package/skills/pc-panel-review/SKILL.md +2 -46
- package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
- package/skills/plan-paper-timeline/SKILL.md +2 -36
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
- package/skills/render-paper/SKILL.md +1 -37
- package/skills/render-paper/SKILL.md.spec.ts +0 -36
- package/skills/research-ideate/SKILL.md +2 -39
- package/skills/research-ideate/SKILL.md.spec.ts +1 -47
- package/skills/study-accepted-papers/SKILL.md +1 -34
- package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
- package/skills/submit-paper/SKILL.md +1 -33
- package/skills/submit-paper/SKILL.md.spec.ts +0 -32
- package/skills/sweep-design-space/SKILL.md +1 -38
- package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
- package/skills/tighten-paper/SKILL.md +2 -39
- package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
- package/skills/tighten-paper/structure.mjs +4 -11
- package/skills/verify-citations/SKILL.md +2 -38
- package/skills/verify-citations/SKILL.md.spec.ts +1 -37
- package/src/adapters/banal/failure.ts +9 -10
- package/src/adapters/banal/index.test.ts +2 -2
- package/src/adapters/banal/index.ts +8 -10
- package/src/adapters/banal/install.ts +9 -4
- package/src/adapters/banal/invocation.test.ts +20 -8
- package/src/adapters/banal/invocation.ts +23 -8
- package/src/adapters/banal/locate.test.ts +2 -2
- package/src/adapters/banal/locate.ts +27 -21
- package/src/adapters/banal/run.test.ts +2 -2
- package/src/adapters/banal/run.ts +16 -10
- package/src/adapters/banal/settings.ts +2 -2
- package/src/adapters/curl/download.test.ts +4 -4
- package/src/adapters/memory/index.ts +4 -3
- package/src/adapters/node/files.test.ts +5 -3
- package/src/adapters/node/process.io.ts +9 -2
- package/src/adapters/node/process.test.ts +4 -4
- package/src/adapters/node/workspace.io.ts +9 -4
- package/src/adapters/references/index.ts +7 -2
- package/src/build-engine.test.ts +11 -6
- package/src/build-engine.ts +10 -5
- package/src/build.test.ts +10 -11
- package/src/build.ts +8 -9
- package/src/caller-path.ts +13 -0
- package/src/cli.harness.mjs +0 -2
- package/src/cli.test.ts +19 -8
- package/src/cli.ts +82 -25
- package/src/doctor.test.ts +1 -1
- package/src/doctor.ts +14 -5
- package/src/domain/paths.test.ts +15 -0
- package/src/domain/paths.ts +20 -1
- package/src/domain/ready.ts +31 -0
- package/src/domain/record.test.ts +20 -0
- package/src/domain/record.ts +12 -0
- package/src/domain/sha256.ts +5 -3
- package/src/engine.ts +30 -2
- package/src/facts-file.ts +43 -28
- package/src/hooks-settings.harness.ts +17 -0
- package/src/hooks-settings.ts +28 -4
- package/src/init.test.ts +25 -5
- package/src/init.ts +26 -12
- package/src/new-paper.harness.mjs +7 -23
- package/src/new-paper.ts +7 -3
- package/src/paper-files.test.ts +22 -19
- package/src/paper-files.ts +16 -27
- package/src/paper-settings.test.ts +2 -2
- package/src/paper-settings.ts +5 -5
- package/src/pdf-facts.test.ts +16 -5
- package/src/pdf-facts.ts +79 -22
- package/src/pdf-geometry.ts +5 -3
- package/src/ports/tool-installer.ts +2 -17
- package/src/presets.ts +28 -14
- package/src/references.test.ts +25 -19
- package/src/references.ts +4 -5
- package/src/rules-config.ts +40 -11
- package/src/tex-requirements.test.ts +21 -0
- package/src/tex-requirements.ts +29 -6
- package/src/toolchain.test.ts +9 -5
- package/src/toolchain.ts +2 -2
- package/src/types.ts +5 -3
- package/src/venue-rules.test.ts +8 -4
- package/src/venue-rules.ts +21 -11
- package/skills/paper-pipeline/scripts/announce.mjs +0 -67
- package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
- package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
- package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
- package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
- package/skills/paper-pipeline/scripts/status.mjs +0 -295
- package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
package/src/pdf-facts.ts
CHANGED
|
@@ -35,12 +35,67 @@ 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
37
|
import { messageOf } from "./domain/text.ts";
|
|
38
|
+
import { fieldOf, isRecord, numbersOf } from "./domain/record.ts";
|
|
38
39
|
|
|
39
40
|
type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
|
|
42
|
+
/** A text item as this module reads it; pdf.js's `TextItem` is one. */
|
|
43
|
+
interface TextItem {
|
|
44
|
+
readonly str: string;
|
|
45
|
+
readonly fontName: string;
|
|
46
|
+
/** pdf.js types it `any[]`; `numbersOf` reads it. */
|
|
47
|
+
readonly transform: readonly unknown[];
|
|
48
|
+
readonly width: number;
|
|
49
|
+
readonly height: number;
|
|
50
|
+
}
|
|
51
|
+
/** What else pdf.js lists among a page's items: a marked-content boundary, with no text. */
|
|
52
|
+
interface MarkedContent {
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly str?: undefined;
|
|
55
|
+
}
|
|
56
|
+
type Item = TextItem | MarkedContent;
|
|
57
|
+
|
|
58
|
+
/** A page's operators, as `fillsFor` walks them; pdf.js's operator list is one. */
|
|
59
|
+
interface OperatorList {
|
|
60
|
+
readonly fnArray: readonly number[];
|
|
61
|
+
readonly argsArray: readonly unknown[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A viewport as this module reads it; pdf.js's `PageViewport` is one. */
|
|
65
|
+
interface Viewport {
|
|
66
|
+
readonly width: number;
|
|
67
|
+
readonly height: number;
|
|
68
|
+
readonly transform: readonly number[];
|
|
69
|
+
convertToViewportPoint(x: number, y: number): readonly unknown[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A page as this module reads it. pdf.js's `PDFPageProxy` is one, and so is a test's fake page: the
|
|
74
|
+
* module depends on these five members, not on the rest of pdf.js's page.
|
|
75
|
+
*/
|
|
76
|
+
interface Page {
|
|
77
|
+
getOperatorList(): Promise<OperatorList>;
|
|
78
|
+
getTextContent(): Promise<{ readonly items: readonly Item[] }>;
|
|
79
|
+
/** pdf.js's `commonObjs`: loaded objects by id, iterable as `[id, object]` pairs. */
|
|
80
|
+
readonly commonObjs: {
|
|
81
|
+
has(id: string): boolean;
|
|
82
|
+
get(id: string): unknown;
|
|
83
|
+
[Symbol.iterator](): Iterator<readonly unknown[]>;
|
|
84
|
+
};
|
|
85
|
+
getViewport(params: { scale: number }): Viewport;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A document as this module reads it; pdf.js's `PDFDocumentProxy` is one. */
|
|
89
|
+
interface Doc {
|
|
90
|
+
readonly numPages: number;
|
|
91
|
+
getPage(pageNumber: number): Promise<Page>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A viewport point, which pdf.js types as `any[]`: the two numbers it holds. */
|
|
95
|
+
const pointOf = (xy: readonly unknown[]): [number, number] => [
|
|
96
|
+
Number(xy[0]),
|
|
97
|
+
Number(xy[1]),
|
|
98
|
+
];
|
|
44
99
|
|
|
45
100
|
/**
|
|
46
101
|
* What `readPdf` measured. `last` is the last page's words; `classifyLastPage` reads it. `layout` is
|
|
@@ -95,8 +150,8 @@ const fail = (reason: PdfReadFailure, detail: string): PdfRead => ({
|
|
|
95
150
|
|
|
96
151
|
/** A font object pdf.js left in `commonObjs`, read into a `RawFont`, or null when it is not one. */
|
|
97
152
|
export function rawFontOf(id: string, obj: unknown): RawFont | null {
|
|
98
|
-
if (
|
|
99
|
-
const o = obj
|
|
153
|
+
if (!isRecord(obj)) return null;
|
|
154
|
+
const o = obj;
|
|
100
155
|
if (!("loadedName" in o) && !("isType3Font" in o)) return null;
|
|
101
156
|
const str = (v: unknown): string | undefined =>
|
|
102
157
|
typeof v === "string" ? v : undefined;
|
|
@@ -111,7 +166,7 @@ export function rawFontOf(id: string, obj: unknown): RawFont | null {
|
|
|
111
166
|
}
|
|
112
167
|
|
|
113
168
|
const isText = (it: Item): it is TextItem =>
|
|
114
|
-
|
|
169
|
+
typeof it.str === "string" && it.str.trim() !== "";
|
|
115
170
|
|
|
116
171
|
/** The font pdf.js resolved for `id`, if it did. `get` throws on an unresolved id; `has` does not. */
|
|
117
172
|
function resolved(page: Page, id: string): unknown {
|
|
@@ -124,9 +179,10 @@ function resolved(page: Page, id: string): unknown {
|
|
|
124
179
|
*/
|
|
125
180
|
function loadedFonts(page: Page): Map<string, RawFont> {
|
|
126
181
|
const out = new Map<string, RawFont>();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
// pdf.js yields `[id, object]` pairs, typed `any[]`.
|
|
183
|
+
for (const entry of page.commonObjs) {
|
|
184
|
+
const f = rawFontOf(String(entry[0]), entry[1]);
|
|
185
|
+
if (f) out.set(f.id, f);
|
|
130
186
|
}
|
|
131
187
|
return out;
|
|
132
188
|
}
|
|
@@ -167,7 +223,7 @@ async function readPage(
|
|
|
167
223
|
* Both are far below any balance tolerance, so the simpler rule stays.
|
|
168
224
|
*/
|
|
169
225
|
function metricsOf(font: unknown): { ascent: number; descent: number } {
|
|
170
|
-
const f = (font
|
|
226
|
+
const f = isRecord(font) ? font : {};
|
|
171
227
|
const ascent = f["ascent"];
|
|
172
228
|
if (typeof ascent !== "number" || !(ascent > 0))
|
|
173
229
|
return { ascent: DEFAULT_ASCENT, descent: DEFAULT_DESCENT };
|
|
@@ -179,8 +235,8 @@ function metricsOf(font: unknown): { ascent: number; descent: number } {
|
|
|
179
235
|
function pageText(page: Page, items: readonly TextItem[]): PageText {
|
|
180
236
|
const vp = page.getViewport({ scale: 1 });
|
|
181
237
|
const runs: TextRun[] = items.map((it) => {
|
|
182
|
-
const [a = 0, b = 0, , d = 0, e = 0, f = 0] = it.transform
|
|
183
|
-
const [x, baseline] = vp.convertToViewportPoint(e, f)
|
|
238
|
+
const [a = 0, b = 0, , d = 0, e = 0, f = 0] = numbersOf(it.transform);
|
|
239
|
+
const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
|
|
184
240
|
const size = Math.hypot(a, b) || it.height || Math.abs(d);
|
|
185
241
|
return {
|
|
186
242
|
text: it.str,
|
|
@@ -200,7 +256,7 @@ function pageText(page: Page, items: readonly TextItem[]): PageText {
|
|
|
200
256
|
|
|
201
257
|
/** The font name pdf.js resolved for an item, else its internal id. */
|
|
202
258
|
function fontNameOf(font: unknown, id: string): string {
|
|
203
|
-
const name = (font
|
|
259
|
+
const name = fieldOf(font, "name");
|
|
204
260
|
return typeof name === "string" && name ? name : id;
|
|
205
261
|
}
|
|
206
262
|
|
|
@@ -211,12 +267,12 @@ function fontNameOf(font: unknown, id: string): string {
|
|
|
211
267
|
*/
|
|
212
268
|
function boxOf(
|
|
213
269
|
page: Page,
|
|
214
|
-
vp:
|
|
270
|
+
vp: Viewport,
|
|
215
271
|
it: TextItem,
|
|
216
272
|
lib: PdfJs,
|
|
217
273
|
): Omit<TextBox, "fill"> {
|
|
218
|
-
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = it.transform
|
|
219
|
-
const [x, baseline] = vp.convertToViewportPoint(e, f)
|
|
274
|
+
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = numbersOf(it.transform);
|
|
275
|
+
const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
|
|
220
276
|
const size = Math.hypot(c, d) || Math.hypot(a, b) || it.height;
|
|
221
277
|
const font = resolved(page, it.fontName);
|
|
222
278
|
const m = metricsOf(font);
|
|
@@ -229,7 +285,7 @@ function boxOf(
|
|
|
229
285
|
font: fontNameOf(font, it.fontName),
|
|
230
286
|
text: it.str,
|
|
231
287
|
upright: isUpright(
|
|
232
|
-
lib.Util.transform(vp.transform, it.transform)
|
|
288
|
+
numbersOf(lib.Util.transform(vp.transform, it.transform)),
|
|
233
289
|
),
|
|
234
290
|
};
|
|
235
291
|
}
|
|
@@ -238,7 +294,7 @@ function boxOf(
|
|
|
238
294
|
function layoutOf(
|
|
239
295
|
page: Page,
|
|
240
296
|
items: readonly TextItem[],
|
|
241
|
-
ops:
|
|
297
|
+
ops: OperatorList,
|
|
242
298
|
lib: PdfJs,
|
|
243
299
|
): PageLayout {
|
|
244
300
|
const vp = page.getViewport({ scale: 1 });
|
|
@@ -290,10 +346,11 @@ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
|
|
|
290
346
|
|
|
291
347
|
/** pdf.js's own exception names for the two conditions a caller can act on. */
|
|
292
348
|
export function failureOf(e: unknown): PdfRead {
|
|
293
|
-
const
|
|
294
|
-
const
|
|
349
|
+
const name = fieldOf(e, "name");
|
|
350
|
+
const message = fieldOf(e, "message");
|
|
351
|
+
const detail = `${typeof name === "string" ? name : "Error"}: ${typeof message === "string" ? message : String(e)}`;
|
|
295
352
|
return fail(
|
|
296
|
-
|
|
353
|
+
name === "PasswordException" ? "encrypted" : "unreadable",
|
|
297
354
|
detail,
|
|
298
355
|
);
|
|
299
356
|
}
|
package/src/pdf-geometry.ts
CHANGED
|
@@ -160,6 +160,10 @@ export type FontProgram =
|
|
|
160
160
|
/** A program pdf.js named but this module does not know — kept verbatim, never guessed. */
|
|
161
161
|
export type UnknownProgram = `unknown:${string}`;
|
|
162
162
|
|
|
163
|
+
const isUnknownProgram = (
|
|
164
|
+
p: FontProgram | UnknownProgram,
|
|
165
|
+
): p is UnknownProgram => p.startsWith("unknown:");
|
|
166
|
+
|
|
163
167
|
/**
|
|
164
168
|
* One font the PDF draws text with.
|
|
165
169
|
*
|
|
@@ -296,7 +300,5 @@ export const POPPLER_TYPE: Readonly<Record<FontProgram, string>> = {
|
|
|
296
300
|
/** The type column for any fact, the unknown ones verbatim. */
|
|
297
301
|
export function popplerType(f: FontFact): string {
|
|
298
302
|
if (f.kind === "type3") return POPPLER_TYPE.Type3;
|
|
299
|
-
return f.program.
|
|
300
|
-
? f.program
|
|
301
|
-
: POPPLER_TYPE[f.program as FontProgram];
|
|
303
|
+
return isUnknownProgram(f.program) ? f.program : POPPLER_TYPE[f.program];
|
|
302
304
|
}
|
|
@@ -2,26 +2,11 @@
|
|
|
2
2
|
* The port `paperlint toolchain` installs and checks an external program through. One port, many
|
|
3
3
|
* programs: banal implements it now, TeX Live in #76.
|
|
4
4
|
*/
|
|
5
|
-
import type { Opaque } from "ts-essentials";
|
|
6
5
|
import type { Result } from "../domain/result.ts";
|
|
7
6
|
import type { Lines } from "../domain/text.ts";
|
|
7
|
+
import type { Ready } from "../domain/ready.ts";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
|
|
11
|
-
* only an installer's adapter — one minting function each (banal's is `ready` in
|
|
12
|
-
* `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
|
|
13
|
-
*/
|
|
14
|
-
export type Ready = Opaque<
|
|
15
|
-
{
|
|
16
|
-
/** Where it is: the program's path or its bin directory. */
|
|
17
|
-
readonly where: string;
|
|
18
|
-
/** This run installed it. */
|
|
19
|
-
readonly fresh: boolean;
|
|
20
|
-
/** What was checked before calling it ready, as a person reads it. */
|
|
21
|
-
readonly verified: string;
|
|
22
|
-
},
|
|
23
|
-
"Ready"
|
|
24
|
-
>;
|
|
9
|
+
export type { Ready };
|
|
25
10
|
|
|
26
11
|
export interface ToolInstaller {
|
|
27
12
|
/** The program as a person reads it: `banal 1.2 (HotCRP f3e4352)`. */
|
package/src/presets.ts
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
type TexRequirements,
|
|
40
40
|
type VenueFormat,
|
|
41
41
|
} from "./tex-requirements.ts";
|
|
42
|
-
import
|
|
42
|
+
import { callerPath } from "./caller-path.ts";
|
|
43
43
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
44
44
|
import type { Files } from "./ports/files.ts";
|
|
45
45
|
import {
|
|
@@ -120,7 +120,7 @@ function readPreset(
|
|
|
120
120
|
): Result<{ file: string; preset: PresetFile }, PresetProblem> {
|
|
121
121
|
const where = fileOf(spec, from, deps.venuesDir);
|
|
122
122
|
if (!where.ok) return where;
|
|
123
|
-
const bytes = deps.files.readBytes(where.value
|
|
123
|
+
const bytes = deps.files.readBytes(callerPath(where.value));
|
|
124
124
|
if (bytes === null)
|
|
125
125
|
return err({ kind: "not-found", spec, file: where.value, shipped: [] });
|
|
126
126
|
try {
|
|
@@ -166,14 +166,22 @@ export function mergeFormat(
|
|
|
166
166
|
parent: VenueFormat,
|
|
167
167
|
child: VenueFormat,
|
|
168
168
|
): VenueFormat {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
// Every field listed: a VenueFormat literal missing one is a compile error.
|
|
170
|
+
const pick = <K extends Exclude<keyof VenueFormat, "kinds">>(
|
|
171
|
+
k: K,
|
|
172
|
+
): VenueFormat[K] => child[k] ?? parent[k];
|
|
173
|
+
return {
|
|
174
|
+
pageWidthIn: pick("pageWidthIn"),
|
|
175
|
+
pageHeightIn: pick("pageHeightIn"),
|
|
176
|
+
columns: pick("columns"),
|
|
177
|
+
bodyPt: pick("bodyPt"),
|
|
178
|
+
bodyPtTol: pick("bodyPtTol"),
|
|
179
|
+
refPtMin: pick("refPtMin"),
|
|
180
|
+
refPtMax: pick("refPtMax"),
|
|
181
|
+
fontsText: pick("fontsText"),
|
|
182
|
+
fontsTitle: pick("fontsTitle"),
|
|
183
|
+
kinds: new Map([...parent.kinds, ...child.kinds]),
|
|
184
|
+
};
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
/** The chain, root first, merged into one preset. Pure. */
|
|
@@ -182,12 +190,18 @@ function merged(
|
|
|
182
190
|
rootFirst: readonly PresetFile[],
|
|
183
191
|
files: readonly string[],
|
|
184
192
|
): Preset {
|
|
185
|
-
const base
|
|
186
|
-
name:
|
|
187
|
-
template:
|
|
193
|
+
const base: {
|
|
194
|
+
name: string | null;
|
|
195
|
+
template: string | null;
|
|
196
|
+
tex: TexRequirements;
|
|
197
|
+
format: VenueFormat;
|
|
198
|
+
rules: Readonly<Record<string, unknown>>;
|
|
199
|
+
} = {
|
|
200
|
+
name: null,
|
|
201
|
+
template: null,
|
|
188
202
|
tex: NO_REQUIREMENTS,
|
|
189
203
|
format: NO_FORMAT,
|
|
190
|
-
rules: {}
|
|
204
|
+
rules: {},
|
|
191
205
|
};
|
|
192
206
|
const m = rootFirst.reduce(
|
|
193
207
|
(acc, p) => ({
|
package/src/references.test.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { describe, expect, it } from "vitest";
|
|
11
11
|
import { ESLint } from "eslint";
|
|
12
|
+
import { eslintConfig } from "./cli.ts";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
15
|
import { ok } from "./domain/result.ts";
|
|
15
|
-
import
|
|
16
|
+
import { absolutePath } from "./domain/paths.ts";
|
|
16
17
|
import type {
|
|
17
18
|
CheckReferences,
|
|
18
19
|
EntryVerdict,
|
|
@@ -78,9 +79,13 @@ const offline: CheckReferences = (_bib, cache) =>
|
|
|
78
79
|
const ctx = (files: ReturnType<typeof memoryFiles>) => ({
|
|
79
80
|
paperDir: PAPER,
|
|
80
81
|
env: {},
|
|
81
|
-
run: (
|
|
82
|
-
readPdf: (
|
|
83
|
-
measure:
|
|
82
|
+
run: () => ({ status: 0 }),
|
|
83
|
+
readPdf: () => Promise.reject(new Error("the references step reads no PDF")),
|
|
84
|
+
measure: {
|
|
85
|
+
measure: () => {
|
|
86
|
+
throw new Error("the references step measures nothing");
|
|
87
|
+
},
|
|
88
|
+
},
|
|
84
89
|
files,
|
|
85
90
|
});
|
|
86
91
|
|
|
@@ -94,32 +99,26 @@ async function build(tex: string, check: CheckReferences) {
|
|
|
94
99
|
return { files, out };
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
/**
|
|
98
|
-
* The rules as an ESLint plugin. Their context type is the narrow one they read (a custom
|
|
99
|
-
* language's SourceCode), which ESLint's JS-centred plugin type does not describe.
|
|
100
|
-
*/
|
|
101
|
-
const asPlugin = (rules: object): ESLint.Plugin => ({ rules }) as ESLint.Plugin;
|
|
102
|
-
|
|
103
102
|
/** Lint `tex` as the paper's paper.tex against what `files` recorded. */
|
|
104
103
|
async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
|
|
105
104
|
files.writeAtomic(
|
|
106
|
-
`${PAPER}/paper.tex`
|
|
105
|
+
absolutePath(`${PAPER}/paper.tex`),
|
|
107
106
|
new TextEncoder().encode(tex),
|
|
108
107
|
);
|
|
109
108
|
const eslint = new ESLint({
|
|
110
109
|
cwd: "/work",
|
|
111
110
|
overrideConfigFile: true,
|
|
112
|
-
overrideConfig: [
|
|
111
|
+
overrideConfig: eslintConfig([
|
|
113
112
|
{
|
|
114
113
|
files: ["**/paper.tex"],
|
|
115
114
|
plugins: {
|
|
116
115
|
tex: { languages: { latex: texLanguage } },
|
|
117
|
-
paper:
|
|
116
|
+
paper: { rules: referenceRules({ files }) },
|
|
118
117
|
},
|
|
119
118
|
language: "tex/latex",
|
|
120
119
|
rules: REFERENCE_RULE_LEVELS,
|
|
121
120
|
},
|
|
122
|
-
],
|
|
121
|
+
]),
|
|
123
122
|
});
|
|
124
123
|
const [res] = await eslint.lintText(tex, {
|
|
125
124
|
filePath: join(PAPER, "paper.tex"),
|
|
@@ -186,7 +185,7 @@ function fetching(key: string) {
|
|
|
186
185
|
return { check, seen };
|
|
187
186
|
}
|
|
188
187
|
const cacheText = (files: ReturnType<typeof memoryFiles>): string | null => {
|
|
189
|
-
const b = files.readBytes(lookupCachePath(PAPER)
|
|
188
|
+
const b = files.readBytes(absolutePath(lookupCachePath(PAPER)));
|
|
190
189
|
return b === null ? null : new TextDecoder().decode(b);
|
|
191
190
|
};
|
|
192
191
|
|
|
@@ -363,7 +362,7 @@ const record = (
|
|
|
363
362
|
) => {
|
|
364
363
|
const bib = present(bibliographyOf(files, PAPER), "the bibliography");
|
|
365
364
|
files.writeAtomic(
|
|
366
|
-
referencesPath(PAPER)
|
|
365
|
+
absolutePath(referencesPath(PAPER)),
|
|
367
366
|
new TextEncoder().encode(
|
|
368
367
|
JSON.stringify({
|
|
369
368
|
schema: 1,
|
|
@@ -424,9 +423,16 @@ describe("the reference rules over refs.bib, and on other files", () => {
|
|
|
424
423
|
const files = memoryFiles({});
|
|
425
424
|
const rules = referenceRules({ files });
|
|
426
425
|
for (const rule of Object.values(rules))
|
|
427
|
-
expect(
|
|
428
|
-
{
|
|
429
|
-
|
|
426
|
+
expect(
|
|
427
|
+
rule.create({
|
|
428
|
+
filename: `${PAPER}/notes.tex`,
|
|
429
|
+
cwd: "/work",
|
|
430
|
+
sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
|
|
431
|
+
report: () => {
|
|
432
|
+
throw new Error("a file that is not paper.tex is never reported");
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
).toEqual({});
|
|
430
436
|
});
|
|
431
437
|
});
|
|
432
438
|
|
package/src/references.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* a pass would be the counter that counts what it never looked at.
|
|
29
29
|
*/
|
|
30
30
|
import { join } from "node:path";
|
|
31
|
-
import
|
|
31
|
+
import { callerPath } from "./caller-path.ts";
|
|
32
32
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
33
33
|
import type { Files } from "./ports/files.ts";
|
|
34
34
|
import {
|
|
@@ -60,9 +60,8 @@ export const lookupCachePath = (paperDir: string): string =>
|
|
|
60
60
|
export const referencesPath = (paperDir: string): string =>
|
|
61
61
|
join(paperDir, "_build", REFERENCES_FILE);
|
|
62
62
|
|
|
63
|
-
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
64
63
|
const text = (files: Files, p: string): string | null => {
|
|
65
|
-
const b = files.readBytes(
|
|
64
|
+
const b = files.readBytes(callerPath(p));
|
|
66
65
|
return b === null ? null : new TextDecoder().decode(b);
|
|
67
66
|
};
|
|
68
67
|
|
|
@@ -186,7 +185,7 @@ export async function recordReferences(
|
|
|
186
185
|
const record = (c: ReferencesCheck): ReferencesDocument => {
|
|
187
186
|
const doc = documentOf(bib, c);
|
|
188
187
|
files.writeAtomic(
|
|
189
|
-
|
|
188
|
+
callerPath(referencesPath(paperDir)),
|
|
190
189
|
new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
|
|
191
190
|
);
|
|
192
191
|
return doc;
|
|
@@ -206,7 +205,7 @@ export async function recordReferences(
|
|
|
206
205
|
const fetched = newAnswers(cache.value, result.cache);
|
|
207
206
|
if (fetched > 0)
|
|
208
207
|
files.writeAtomic(
|
|
209
|
-
|
|
208
|
+
callerPath(lookupCachePath(paperDir)),
|
|
210
209
|
new TextEncoder().encode(serializeLookupCache(result.cache)),
|
|
211
210
|
);
|
|
212
211
|
const bad = doc.entries.filter(
|
package/src/rules-config.ts
CHANGED
|
@@ -39,11 +39,40 @@ export interface RuleBlock {
|
|
|
39
39
|
readonly rules: Readonly<Record<string, RuleEntry>>;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A plugin as a config block registers it, typed to what paperlint reads of it: its rules, and the
|
|
44
|
+
* languages it brings (`tex` brings LaTeX).
|
|
45
|
+
*/
|
|
46
|
+
export interface ConfigPlugin {
|
|
47
|
+
readonly rules?: Readonly<Record<string, unknown>>;
|
|
48
|
+
readonly languages?: Readonly<Record<string, unknown>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An ESLint config block as paperlint builds and reads one. Not `Linter.Config`: ESLint types a
|
|
53
|
+
* plugin's rule as one that runs on ANY language, and paperlint's rules are written for the language
|
|
54
|
+
* they read (LaTeX, markdown), so none of them is assignable to it. ESLint checks a block when it
|
|
55
|
+
* loads it; `eslintConfig` in cli.ts is the one place these blocks become `Linter.Config`.
|
|
56
|
+
*/
|
|
57
|
+
export interface ConfigBlock {
|
|
58
|
+
readonly name?: string;
|
|
59
|
+
readonly basePath?: string;
|
|
60
|
+
readonly files?: readonly (string | readonly string[])[];
|
|
61
|
+
readonly ignores?: readonly string[];
|
|
62
|
+
readonly plugins?: Readonly<Record<string, ConfigPlugin>>;
|
|
63
|
+
readonly language?: string;
|
|
64
|
+
readonly languageOptions?: Readonly<Record<string, unknown>>;
|
|
65
|
+
readonly linterOptions?: Readonly<Record<string, unknown>>;
|
|
66
|
+
readonly rules?: Readonly<Record<string, unknown>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
export type Parsed<T> =
|
|
43
70
|
| { readonly ok: true; readonly value: T }
|
|
44
71
|
| { readonly ok: false; readonly error: string };
|
|
45
72
|
|
|
46
|
-
const SEVERITIES: readonly
|
|
73
|
+
const SEVERITIES: readonly Severity[] = ["off", "warn", "error", 0, 1, 2];
|
|
74
|
+
const isSeverity = (v: unknown): v is Severity =>
|
|
75
|
+
SEVERITIES.some((s) => s === v);
|
|
47
76
|
|
|
48
77
|
const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
|
|
49
78
|
|
|
@@ -61,9 +90,10 @@ const isStringList = (v: unknown): v is string[] =>
|
|
|
61
90
|
|
|
62
91
|
/** A severity, or `[severity, ...options]`. */
|
|
63
92
|
function parseEntry(v: unknown): RuleEntry | null {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
const entry: readonly unknown[] = Array.isArray(v) ? v : [v];
|
|
94
|
+
const [sev, ...options] = entry;
|
|
95
|
+
if (!isSeverity(sev)) return null;
|
|
96
|
+
return Array.isArray(v) ? [sev, ...options] : sev;
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
/**
|
|
@@ -105,8 +135,8 @@ function badGlobs(o: Record<string, unknown>, at: string): string | null {
|
|
|
105
135
|
|
|
106
136
|
/** The block's `files`/`ignores`, present only when given. */
|
|
107
137
|
const globsOf = (o: Record<string, unknown>) => ({
|
|
108
|
-
...(o["files"] ? { files: o["files"]
|
|
109
|
-
...(o["ignores"] ? { ignores: o["ignores"]
|
|
138
|
+
...(isStringList(o["files"]) ? { files: o["files"] } : {}),
|
|
139
|
+
...(isStringList(o["ignores"]) ? { ignores: o["ignores"] } : {}),
|
|
110
140
|
});
|
|
111
141
|
|
|
112
142
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
@@ -182,16 +212,15 @@ export function parseRuleBlocks(
|
|
|
182
212
|
* `foreign` (a dependency's plugin, such as `@eslint/markdown`) are not paperlint's and are skipped.
|
|
183
213
|
*/
|
|
184
214
|
export function shippedRuleIds(
|
|
185
|
-
config: readonly
|
|
215
|
+
config: readonly ConfigBlock[],
|
|
186
216
|
foreign: readonly unknown[],
|
|
187
217
|
): Set<string> {
|
|
188
218
|
const ids = new Set<string>();
|
|
189
219
|
for (const block of config) {
|
|
190
|
-
const
|
|
191
|
-
for (const [name, plugin] of Object.entries(plugins ?? {})) {
|
|
220
|
+
for (const [name, plugin] of Object.entries(block.plugins ?? {})) {
|
|
192
221
|
if (foreign.includes(plugin)) continue;
|
|
193
|
-
const
|
|
194
|
-
|
|
222
|
+
for (const rule of Object.keys(plugin.rules ?? {}))
|
|
223
|
+
ids.add(`${name}/${rule}`);
|
|
195
224
|
}
|
|
196
225
|
}
|
|
197
226
|
return ids;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { loadTypescript } from "./tex-requirements.ts";
|
|
4
|
+
|
|
5
|
+
test("loadTypescript refuses a module without the two functions a profile is read with", () => {
|
|
6
|
+
assert.throws(
|
|
7
|
+
() => loadTypescript(() => ({})),
|
|
8
|
+
/loaded without parseConfigFileTextToJson/,
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("loadTypescript hands back what `require` loaded when it has them", () => {
|
|
13
|
+
const ts = {
|
|
14
|
+
parseConfigFileTextToJson: () => ({ config: {} }),
|
|
15
|
+
flattenDiagnosticMessageText: () => "",
|
|
16
|
+
};
|
|
17
|
+
assert.equal(
|
|
18
|
+
loadTypescript(() => ts),
|
|
19
|
+
ts,
|
|
20
|
+
);
|
|
21
|
+
});
|
package/src/tex-requirements.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { join } from "node:path";
|
|
|
27
27
|
import Ajv from "ajv";
|
|
28
28
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
29
29
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
30
|
+
import { fieldOf } from "./domain/record.ts";
|
|
30
31
|
|
|
31
32
|
/** CTAN package name → the names that prove it is installed. */
|
|
32
33
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -51,15 +52,37 @@ const PROFILE_EXT = ".jsonc";
|
|
|
51
52
|
|
|
52
53
|
export const NO_REQUIREMENTS: TexRequirements = { packages: {}, tools: {} };
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/** The two functions of `typescript` a profile is read with. */
|
|
56
|
+
type TsJsonc = Pick<
|
|
57
|
+
typeof import("typescript"),
|
|
58
|
+
"parseConfigFileTextToJson" | "flattenDiagnosticMessageText"
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
const isTsJsonc = (m: unknown): m is TsJsonc =>
|
|
62
|
+
typeof fieldOf(m, "parseConfigFileTextToJson") === "function" &&
|
|
63
|
+
typeof fieldOf(m, "flattenDiagnosticMessageText") === "function";
|
|
64
|
+
|
|
65
|
+
/** `typescript` as `load` returns it — `require` answers `any` — checked for the two functions used. */
|
|
66
|
+
export function loadTypescript(load: (id: string) => unknown): TsJsonc {
|
|
67
|
+
const m = load("typescript");
|
|
68
|
+
if (!isTsJsonc(m))
|
|
69
|
+
throw new Error(
|
|
70
|
+
"`typescript` loaded without parseConfigFileTextToJson — reinstall this package",
|
|
71
|
+
);
|
|
72
|
+
return m;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let ts: TsJsonc | null = null;
|
|
56
76
|
/** TypeScript is loaded only when a profile is read: `paperlint lint` never pays for it. */
|
|
57
|
-
function typescript():
|
|
58
|
-
ts ??= createRequire(import.meta.url)
|
|
77
|
+
function typescript(): TsJsonc {
|
|
78
|
+
ts ??= loadTypescript(createRequire(import.meta.url));
|
|
59
79
|
return ts;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
type Validate = ReturnType<Ajv.Ajv["compile"]>;
|
|
83
|
+
/** `PresetJson` is the TypeScript side of the profile schema: a document the schema accepts is one. */
|
|
84
|
+
const matchesSchema = (v: unknown, validate: Validate): v is PresetJson =>
|
|
85
|
+
validate(v) === true;
|
|
63
86
|
const validators = new Map<string, Validate>();
|
|
64
87
|
function validatorFor(dir: string): Validate {
|
|
65
88
|
const known = validators.get(dir);
|
|
@@ -223,11 +246,11 @@ export function parsePreset(
|
|
|
223
246
|
`${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
|
|
224
247
|
);
|
|
225
248
|
const validate = validatorFor(dir);
|
|
226
|
-
if (!
|
|
249
|
+
if (!matchesSchema(config, validate))
|
|
227
250
|
throw new Error(
|
|
228
251
|
`${file} does not match ${SCHEMA_FILE}:\n ${violations(file, validate).join("\n ")}`,
|
|
229
252
|
);
|
|
230
|
-
const j = config
|
|
253
|
+
const j = config;
|
|
231
254
|
return {
|
|
232
255
|
extends: orNull(j.extends),
|
|
233
256
|
name: orNull(j.name),
|
package/src/toolchain.test.ts
CHANGED
|
@@ -34,19 +34,19 @@ type Answer = {
|
|
|
34
34
|
};
|
|
35
35
|
/** A runner answering by program name (the last path segment); `onRun` may write files first. */
|
|
36
36
|
function io(
|
|
37
|
-
answers: Record<string, (args: string[]) => Answer>,
|
|
37
|
+
answers: Record<string, (args: readonly string[]) => Answer>,
|
|
38
38
|
): ToolchainIO & { lines: string[] } {
|
|
39
39
|
const lines: string[] = [];
|
|
40
40
|
return {
|
|
41
41
|
lines,
|
|
42
42
|
log: (l) => lines.push(l),
|
|
43
43
|
env: {},
|
|
44
|
-
run: (
|
|
44
|
+
run: (cmd, args) => {
|
|
45
45
|
const name = cmd.split("/").pop() ?? cmd;
|
|
46
46
|
const answer = answers[`${name} ${args[0] ?? ""}`] ?? answers[name];
|
|
47
47
|
if (!answer) throw new Error(`unexpected ${name} ${args.join(" ")}`);
|
|
48
48
|
return answer(args);
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
const ok = (stdout = ""): Answer => ({ status: 0, stdout, stderr: "" });
|
|
@@ -194,7 +194,7 @@ function cacheWith(name: string, year: string): string {
|
|
|
194
194
|
const ACMART = { packages: { acmart: ["acmart.cls"] }, tools: {} };
|
|
195
195
|
function ensure(
|
|
196
196
|
cache: string,
|
|
197
|
-
answers: Record<string, (args: string[]) => Answer>,
|
|
197
|
+
answers: Record<string, (args: readonly string[]) => Answer>,
|
|
198
198
|
) {
|
|
199
199
|
const t = io(answers);
|
|
200
200
|
const errs: string[] = [];
|
|
@@ -272,7 +272,11 @@ test("runToolchain with only its installer: the console is the default output",
|
|
|
272
272
|
const saved = console.error;
|
|
273
273
|
console.error = (...a: unknown[]) => void seen.push(a.join(" "));
|
|
274
274
|
try {
|
|
275
|
-
|
|
275
|
+
const unused = (): never => {
|
|
276
|
+
throw new Error("an unsupported platform installs nothing");
|
|
277
|
+
};
|
|
278
|
+
const banal = { label: "banal", ensure: unused, check: unused };
|
|
279
|
+
assert.equal(runToolchain({ platform: "win32", banal }), 1);
|
|
276
280
|
} finally {
|
|
277
281
|
console.error = saved;
|
|
278
282
|
}
|