paperlint 8.0.0 → 9.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 +9 -25
- package/CONTRIBUTING.md +5 -4
- package/README.md +4 -3
- package/dist/adapters/bibtex/index.d.ts +3 -0
- package/dist/adapters/bibtex/index.d.ts.map +1 -0
- package/dist/adapters/bibtex/index.js +103 -0
- package/dist/adapters/bibtex/index.js.map +1 -0
- package/dist/adapters/latex/filecontents.d.ts +33 -0
- package/dist/adapters/latex/filecontents.d.ts.map +1 -0
- package/dist/adapters/latex/filecontents.js +65 -0
- package/dist/adapters/latex/filecontents.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +3 -3
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +18 -10
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +12 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +39 -0
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +7 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +55 -6
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +9 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +27 -24
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/sources-record/index.d.ts +9 -0
- package/dist/adapters/sources-record/index.d.ts.map +1 -0
- package/dist/adapters/sources-record/index.js +68 -0
- package/dist/adapters/sources-record/index.js.map +1 -0
- package/dist/adapters/tex-output/blg.d.ts +9 -0
- package/dist/adapters/tex-output/blg.d.ts.map +1 -0
- package/dist/adapters/tex-output/blg.js +20 -0
- package/dist/adapters/tex-output/blg.js.map +1 -0
- package/dist/adapters/tex-output/fls.d.ts +8 -0
- package/dist/adapters/tex-output/fls.d.ts.map +1 -0
- package/dist/adapters/tex-output/fls.js +16 -0
- package/dist/adapters/tex-output/fls.js.map +1 -0
- package/dist/adapters/tex-output/index.d.ts +7 -0
- package/dist/adapters/tex-output/index.d.ts.map +1 -0
- package/dist/adapters/tex-output/index.js +7 -0
- package/dist/adapters/tex-output/index.js.map +1 -0
- package/dist/bib-rules.d.ts +47 -0
- package/dist/bib-rules.d.ts.map +1 -0
- package/dist/bib-rules.js +127 -0
- package/dist/bib-rules.js.map +1 -0
- package/dist/build.d.ts +29 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -24
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -26
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +1 -10
- package/dist/domain/paper-source.d.ts.map +1 -1
- package/dist/domain/paper-source.js +34 -24
- package/dist/domain/paper-source.js.map +1 -1
- package/dist/domain/paper-sources.d.ts +72 -0
- package/dist/domain/paper-sources.d.ts.map +1 -0
- package/dist/domain/paper-sources.js +18 -0
- package/dist/domain/paper-sources.js.map +1 -0
- package/dist/domain/sources-record.d.ts +102 -0
- package/dist/domain/sources-record.d.ts.map +1 -0
- package/dist/domain/sources-record.js +38 -0
- package/dist/domain/sources-record.js.map +1 -0
- package/dist/domain/tex-run.d.ts +25 -0
- package/dist/domain/tex-run.d.ts.map +1 -0
- package/dist/domain/tex-run.js +79 -0
- package/dist/domain/tex-run.js.map +1 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +4 -3
- package/dist/eslint-rules/paper-context.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +0 -9
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +12 -5
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/latex-log.d.ts +10 -0
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +10 -0
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -0
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js.map +1 -1
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +3 -2
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-includes.d.ts +4 -7
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +29 -21
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-prose.d.ts +2 -2
- package/dist/paper-record.d.ts +25 -0
- package/dist/paper-record.d.ts.map +1 -0
- package/dist/paper-record.js +39 -0
- package/dist/paper-record.js.map +1 -0
- package/dist/paper-sources.d.ts +66 -0
- package/dist/paper-sources.d.ts.map +1 -0
- package/dist/paper-sources.js +92 -0
- package/dist/paper-sources.js.map +1 -0
- package/dist/ports/bib-reader.d.ts +18 -0
- package/dist/ports/bib-reader.d.ts.map +1 -0
- package/dist/ports/bib-reader.js +2 -0
- package/dist/ports/bib-reader.js.map +1 -0
- package/dist/ports/check-references.d.ts +9 -4
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/latex.d.ts +15 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/sources-codec.d.ts +11 -0
- package/dist/ports/sources-codec.d.ts.map +1 -0
- package/dist/ports/sources-codec.js +2 -0
- package/dist/ports/sources-codec.js.map +1 -0
- package/dist/ports/tex-output.d.ts +17 -0
- package/dist/ports/tex-output.d.ts.map +1 -0
- package/dist/ports/tex-output.js +2 -0
- package/dist/ports/tex-output.js.map +1 -0
- package/dist/recorded-bibliography.d.ts +89 -0
- package/dist/recorded-bibliography.d.ts.map +1 -0
- package/dist/recorded-bibliography.js +136 -0
- package/dist/recorded-bibliography.js.map +1 -0
- package/dist/reference-rules.d.ts +5 -5
- package/dist/reference-rules.d.ts.map +1 -1
- package/dist/reference-rules.js +150 -77
- package/dist/reference-rules.js.map +1 -1
- package/dist/references.d.ts +71 -16
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +110 -26
- package/dist/references.js.map +1 -1
- package/dist/register-bands.d.ts +3 -3
- package/dist/register-bands.d.ts.map +1 -1
- package/dist/register-bands.js.map +1 -1
- package/dist/register.d.ts +2 -2
- package/dist/sources-record.d.ts +38 -0
- package/dist/sources-record.d.ts.map +1 -0
- package/dist/sources-record.js +123 -0
- package/dist/sources-record.js.map +1 -0
- package/dist/sources-rules.d.ts +44 -0
- package/dist/sources-rules.d.ts.map +1 -0
- package/dist/sources-rules.js +76 -0
- package/dist/sources-rules.js.map +1 -0
- package/dist/tex-paper.d.ts +10 -15
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +24 -40
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +3 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +12 -4
- package/docs/design/paper-sources.md +197 -0
- package/docs/package-shape-options.md +1 -1
- package/docs/references.md +34 -8
- package/docs/rules/bib/reachable-entry.md +64 -0
- package/docs/rules/paper/sources-fresh.md +81 -0
- package/docs/rules.md +62 -57
- package/eslint-rules/paper-context.ts +4 -3
- package/eslint-rules/paper-typography.test.mjs +12 -72
- package/eslint-rules/paper-typography.ts +13 -6
- package/eslint.config.mjs +22 -0
- package/fixtures/build-e2e/aidc/paper.tex +1 -1
- package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
- package/fixtures/build-e2e/anon-author/paper.tex +1 -1
- package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
- package/fixtures/build-e2e/anon-href/paper.tex +1 -1
- package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
- package/fixtures/paper-sources/p1/paper.tex +30 -0
- package/fixtures/paper-sources/p1/refs.bib +6 -0
- package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
- package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
- package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
- package/fixtures/paper-sources/p1/tex-truth.json +37 -0
- package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
- package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
- package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
- package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
- package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
- package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
- package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
- package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
- package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
- package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
- package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
- package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
- package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
- package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
- package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
- package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
- package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
- package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
- package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
- package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
- package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
- package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
- package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
- package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
- package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
- package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
- package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
- package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
- package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
- package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
- package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
- package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
- package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
- package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
- package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
- package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
- package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
- package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
- package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
- package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
- package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
- package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
- package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
- package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
- package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
- package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
- package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
- package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
- package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
- package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
- package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
- package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
- package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
- package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
- package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
- package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
- package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
- package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
- package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
- package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
- package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
- package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
- package/package.json +2 -10
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/grade-paper-writing/SKILL.md +4 -3
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
- package/skills/grade-paper-writing/prose-lint.mjs +11 -2
- package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
- package/skills/verify-citations/SKILL.md +7 -4
- package/skills/verify-citations/SKILL.md.spec.ts +6 -3
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
- package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
- package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
- package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
- package/src/adapters/bibtex/index.test.ts +163 -0
- package/src/adapters/bibtex/index.ts +155 -0
- package/src/adapters/latex/filecontents.ts +114 -0
- package/src/adapters/latex/includes.ts +18 -10
- package/src/adapters/latex/index.test.ts +73 -0
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/nodes.ts +64 -0
- package/src/adapters/latex/parse.test.ts +38 -0
- package/src/adapters/latex/parse.ts +67 -6
- package/src/adapters/memory/index.ts +17 -0
- package/src/adapters/references/index.test.ts +75 -21
- package/src/adapters/references/index.ts +37 -32
- package/src/adapters/sources-record/index.test.ts +87 -0
- package/src/adapters/sources-record/index.ts +82 -0
- package/src/adapters/tex-output/blg.ts +27 -0
- package/src/adapters/tex-output/fls.ts +24 -0
- package/src/adapters/tex-output/index.test.ts +171 -0
- package/src/adapters/tex-output/index.ts +12 -0
- package/src/bib-rules.test.ts +356 -0
- package/src/bib-rules.ts +228 -0
- package/src/bibliography-at.test.ts +286 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +100 -35
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +20 -10
- package/src/cli.test.ts +14 -10
- package/src/cli.ts +37 -28
- package/src/domain/paper-source.test.ts +58 -20
- package/src/domain/paper-source.ts +38 -42
- package/src/domain/paper-sources.ts +94 -0
- package/src/domain/sources-record.test.ts +71 -0
- package/src/domain/sources-record.ts +136 -0
- package/src/domain/tex-run.test.ts +228 -0
- package/src/domain/tex-run.ts +139 -0
- package/src/heading-case.test.ts +24 -10
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +3 -2
- package/src/new-paper.test.ts +9 -5
- package/src/package-dirs.ts +3 -2
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +34 -5
- package/src/paper-prose.ts +2 -2
- package/src/paper-record.test.ts +173 -0
- package/src/paper-record.ts +66 -0
- package/src/paper-settings-commands.test.ts +5 -2
- package/src/paper-sources.ts +151 -0
- package/src/ports/bib-reader.ts +27 -0
- package/src/ports/check-references.ts +7 -3
- package/src/ports/latex.ts +19 -0
- package/src/ports/sources-codec.ts +11 -0
- package/src/ports/tex-output.ts +17 -0
- package/src/recorded-bibliography.ts +297 -0
- package/src/reference-rules.ts +217 -104
- package/src/references.test.ts +520 -177
- package/src/references.ts +185 -37
- package/src/register-bands.test.ts +5 -1
- package/src/register-bands.ts +3 -5
- package/src/register.test.ts +6 -1
- package/src/register.ts +2 -2
- package/src/rule-cases.test.ts +68 -12
- package/src/rule-docs.test.ts +0 -1
- package/src/sources-record.test.ts +308 -0
- package/src/sources-record.ts +231 -0
- package/src/sources-rules.test.ts +152 -0
- package/src/sources-rules.ts +117 -0
- package/src/tex-paper.test.ts +148 -78
- package/src/tex-paper.ts +31 -61
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
- package/dist/eslint-rules/bib-reachable-entry.js +0 -75
- package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
- package/eslint-rules/bib-reachable-entry.ts +0 -76
package/src/cli.test.ts
CHANGED
|
@@ -33,13 +33,18 @@ import { texInputsDir } from "./package-dirs.ts";
|
|
|
33
33
|
const root = useTempDir("cli-test-");
|
|
34
34
|
|
|
35
35
|
/** `run(argv)` from `cwd`, the output as one string per stream. */
|
|
36
|
-
async function cli(
|
|
36
|
+
async function cli(
|
|
37
|
+
argv: string[],
|
|
38
|
+
cwd: string,
|
|
39
|
+
deps: Pick<NonNullable<Parameters<typeof run>[1]>, "loadTexLanguage"> = {},
|
|
40
|
+
) {
|
|
37
41
|
const out: string[] = [];
|
|
38
42
|
const err: string[] = [];
|
|
39
43
|
const code = await run(argv, {
|
|
40
44
|
log: (...a: unknown[]) => out.push(a.join(" ")),
|
|
41
45
|
err: (...a: unknown[]) => err.push(a.join(" ")),
|
|
42
46
|
cwd,
|
|
47
|
+
...deps,
|
|
43
48
|
});
|
|
44
49
|
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
45
50
|
}
|
|
@@ -617,21 +622,20 @@ test("init's TeX Live install runs the toolchain over the project's papers, neve
|
|
|
617
622
|
assert.equal(typeof here.installed(), "boolean");
|
|
618
623
|
});
|
|
619
624
|
|
|
620
|
-
test("lint with its LaTeX language missing from the install still lints the scorecards", () => {
|
|
625
|
+
test("lint with its LaTeX language missing from the install still lints the scorecards", async () => {
|
|
621
626
|
const dir = writeTree(join(root, "lint-no-tex-language"), {
|
|
622
627
|
"papers/p/PIPELINE-STATUS.md": "---\nstages: []\n---\n# S\n",
|
|
623
|
-
"no-latex.mjs":
|
|
624
|
-
"import { register } from 'node:module';\n" +
|
|
625
|
-
"register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '#eslint-rules/latex-language') throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
|
|
626
628
|
});
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
nodeArgs: ["--import", join(dir, "no-latex.mjs")],
|
|
629
|
+
const gone = Object.assign(new Error("gone"), {
|
|
630
|
+
code: "ERR_MODULE_NOT_FOUND",
|
|
630
631
|
});
|
|
631
|
-
const
|
|
632
|
+
const without = await cli(["lint", "papers"], dir, {
|
|
633
|
+
loadTexLanguage: () => Promise.reject(gone),
|
|
634
|
+
});
|
|
635
|
+
const whole = await cli(["lint", "papers"], dir);
|
|
632
636
|
// A scorecard needs no LaTeX language: the same report, the same code.
|
|
633
637
|
assert.deepEqual(without, whole);
|
|
634
|
-
assert.match(whole.
|
|
638
|
+
assert.match(whole.out, /missing all of `paper\.tex`/);
|
|
635
639
|
});
|
|
636
640
|
|
|
637
641
|
test("`paperlint hook` with no name: exit 2, and the usage names an example", async () => {
|
package/src/cli.ts
CHANGED
|
@@ -46,11 +46,11 @@ import {
|
|
|
46
46
|
FRAGMENT_FILES,
|
|
47
47
|
includeBlocks,
|
|
48
48
|
paperBodies,
|
|
49
|
-
unreadLines,
|
|
50
49
|
} from "./paper-includes.ts";
|
|
51
50
|
import { prepareEngine } from "./build-engine.ts";
|
|
52
51
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
53
52
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
53
|
+
import { bibReader } from "./adapters/bibtex/index.ts";
|
|
54
54
|
import { curlDownload } from "./adapters/curl/index.ts";
|
|
55
55
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
56
56
|
import {
|
|
@@ -93,6 +93,12 @@ import {
|
|
|
93
93
|
type PaperSettings,
|
|
94
94
|
} from "./paper-settings.ts";
|
|
95
95
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
96
|
+
import { SOURCES_RULE_LEVELS, sourcesRules } from "./sources-rules.ts";
|
|
97
|
+
import { sourcesRecorder } from "./sources-record.ts";
|
|
98
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
99
|
+
import { texOutput } from "./adapters/tex-output/index.ts";
|
|
100
|
+
import { BIB_RULE_LEVELS, bibRules } from "./bib-rules.ts";
|
|
101
|
+
import { bibliographyReader } from "./references.ts";
|
|
96
102
|
import { onlineReferences } from "./adapters/references/index.ts";
|
|
97
103
|
import { hotcrpPortal } from "./adapters/hotcrp/index.ts";
|
|
98
104
|
import { runSubmission } from "./submission.ts";
|
|
@@ -166,7 +172,6 @@ import {
|
|
|
166
172
|
} from "./folder-venue-rule.ts";
|
|
167
173
|
import typography from "#eslint-rules/paper-typography";
|
|
168
174
|
import texBuild from "#eslint-rules/tex-build";
|
|
169
|
-
import bibReachable from "#eslint-rules/bib-reachable-entry";
|
|
170
175
|
import reviewFrontmatter from "#eslint-rules/review-frontmatter";
|
|
171
176
|
import siblingFrontmatter from "#eslint-rules/sibling-frontmatter";
|
|
172
177
|
import pdfRules from "#eslint-rules/pdf-last-page-balance";
|
|
@@ -302,11 +307,17 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
302
307
|
unknown key, in either file, is an error.
|
|
303
308
|
`;
|
|
304
309
|
|
|
305
|
-
/** What the
|
|
310
|
+
/** What reads the build's record of a paper: the disk, and the schema its text is parsed with. */
|
|
311
|
+
const RECORD_DEPS = { files: nodeFiles, codec: sourcesCodec };
|
|
312
|
+
|
|
313
|
+
/** What reads the bibliography the last build recorded: the record, and the readers of its databases and blocks. */
|
|
314
|
+
const RECORDED_DEPS = { ...RECORD_DEPS, latex: latexReader, bib: bibReader };
|
|
315
|
+
|
|
316
|
+
/** What the rules over a paper.tex read with: the paper's disk and LaTeX reader, the build's record, and the shipped presets. */
|
|
306
317
|
const TEX_RULE_DEPS = {
|
|
307
|
-
|
|
308
|
-
venuesDir: presetsDir(),
|
|
318
|
+
...RECORD_DEPS,
|
|
309
319
|
latex: latexReader,
|
|
320
|
+
venuesDir: presetsDir(),
|
|
310
321
|
};
|
|
311
322
|
|
|
312
323
|
/** The register band rules and `tex/heading-case`: the rules of the `tex` plugin that read a paper's words. */
|
|
@@ -354,7 +365,8 @@ export function buildConfig(
|
|
|
354
365
|
// The reference rules judge `_build/references.json`, and only on `paper.tex`.
|
|
355
366
|
const texPaperRules = {
|
|
356
367
|
...paperRules,
|
|
357
|
-
...referenceRules(
|
|
368
|
+
...referenceRules(RECORDED_DEPS),
|
|
369
|
+
...sourcesRules(RECORD_DEPS),
|
|
358
370
|
};
|
|
359
371
|
// Each typography rule reports every occurrence where it is, and fixes it (`--fix`).
|
|
360
372
|
const prose = {
|
|
@@ -445,7 +457,7 @@ export function buildConfig(
|
|
|
445
457
|
},
|
|
446
458
|
},
|
|
447
459
|
paper: { rules: texPaperRules },
|
|
448
|
-
bib:
|
|
460
|
+
bib: { rules: bibRules(RECORDED_DEPS) },
|
|
449
461
|
talk: { rules: talkRules(TALK_RULE_DEPS) },
|
|
450
462
|
format: {
|
|
451
463
|
rules: {
|
|
@@ -459,8 +471,9 @@ export function buildConfig(
|
|
|
459
471
|
rules: {
|
|
460
472
|
...prose,
|
|
461
473
|
...REFERENCE_RULE_LEVELS,
|
|
474
|
+
...SOURCES_RULE_LEVELS,
|
|
462
475
|
"paper/figure-ref-style": "warn",
|
|
463
|
-
|
|
476
|
+
...BIB_RULE_LEVELS,
|
|
464
477
|
"tex/future-promise": "warn",
|
|
465
478
|
// A number with no owner: a prose rule, on beside the others above.
|
|
466
479
|
"tex/claim-provenance": "warn",
|
|
@@ -1443,6 +1456,12 @@ async function runBuild(
|
|
|
1443
1456
|
dryRun: a.dryRun,
|
|
1444
1457
|
log,
|
|
1445
1458
|
checkReferences,
|
|
1459
|
+
readBibliography: bibliographyReader(RECORDED_DEPS),
|
|
1460
|
+
recordSources: sourcesRecorder({
|
|
1461
|
+
files: nodeFiles,
|
|
1462
|
+
listDir: nodeListDir,
|
|
1463
|
+
texOutput,
|
|
1464
|
+
}),
|
|
1446
1465
|
engine: () => engineEnv(targets, a, { log, err }),
|
|
1447
1466
|
});
|
|
1448
1467
|
if (out.kind === "no-engine") return 1;
|
|
@@ -1656,10 +1675,17 @@ export async function run(
|
|
|
1656
1675
|
err = console.error,
|
|
1657
1676
|
cwd = process.cwd(),
|
|
1658
1677
|
checkReferences = onlineReferences,
|
|
1678
|
+
loadTexLanguage = async () =>
|
|
1679
|
+
(await import("#eslint-rules/latex-language")).texLanguage,
|
|
1659
1680
|
}: {
|
|
1660
1681
|
log?: typeof console.log;
|
|
1661
1682
|
err?: typeof console.error;
|
|
1662
1683
|
cwd?: string;
|
|
1684
|
+
/**
|
|
1685
|
+
* `lint`'s LaTeX language, loaded lazily because only `lint` needs it. A test hands one that
|
|
1686
|
+
* fails, as a broken install would.
|
|
1687
|
+
*/
|
|
1688
|
+
loadTexLanguage?: () => Promise<unknown>;
|
|
1663
1689
|
/**
|
|
1664
1690
|
* `build`'s online reference check — the real services by default. The e2e build passes a
|
|
1665
1691
|
* fake: no test depends on Crossref, Semantic Scholar or DBLP answering.
|
|
@@ -1765,31 +1791,14 @@ export async function run(
|
|
|
1765
1791
|
// Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
|
|
1766
1792
|
// the file missing from the package), lint still runs over the scorecards and the review notes
|
|
1767
1793
|
// rather than failing outright — the LaTeX rules simply have no language to run in.
|
|
1768
|
-
|
|
1769
|
-
try {
|
|
1770
|
-
({ texLanguage } = await import("#eslint-rules/latex-language"));
|
|
1771
|
-
} catch {
|
|
1772
|
-
texLanguage = null;
|
|
1773
|
-
}
|
|
1794
|
+
const texLanguage: unknown = await loadTexLanguage().catch(() => null);
|
|
1774
1795
|
|
|
1775
|
-
// Each paper's body files,
|
|
1796
|
+
// Each paper's body files, as the last build recorded TeX reading them — only when the LaTeX language
|
|
1776
1797
|
// loaded, since only its rules read them. The project's papers too: `paperlint lint .` reaches
|
|
1777
1798
|
// papers two levels down, which a directory's immediate papers do not.
|
|
1778
1799
|
const bodies = texLanguage
|
|
1779
|
-
? paperBodies(paperDirs([...paths, ...papersRoots(cfg)]),
|
|
1780
|
-
files: nodeFiles,
|
|
1781
|
-
latex: latexReader,
|
|
1782
|
-
})
|
|
1800
|
+
? paperBodies(paperDirs([...paths, ...papersRoots(cfg)]), RECORD_DEPS)
|
|
1783
1801
|
: [];
|
|
1784
|
-
// Named only for the papers this run lints: a paper elsewhere in the project is not this run's.
|
|
1785
|
-
const inScope = (dir: string) =>
|
|
1786
|
-
paths.some((p) => dir === p || dir.startsWith(`${p}${sep}`));
|
|
1787
|
-
unreadLines(
|
|
1788
|
-
bodies.filter((b) => inScope(b.dir)),
|
|
1789
|
-
(p) => shown(cwd, p),
|
|
1790
|
-
).forEach((l) => {
|
|
1791
|
-
err(l);
|
|
1792
|
-
});
|
|
1793
1802
|
|
|
1794
1803
|
const eslint = new ESLint({
|
|
1795
1804
|
cwd: lintRoot(root, paths),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The paper as one text: includes spliced where they stand, every character mapped back to its
|
|
3
|
-
* file, a
|
|
3
|
+
* file, a file with nothing behind it left out, a subfile's body only, and a file that includes itself entered once.
|
|
4
4
|
* The reader is a stand-in that finds `\input{…}` by position, so these tests are about assembling,
|
|
5
5
|
* not about parsing LaTeX (the LaTeX adapter's own tests cover `includes`).
|
|
6
6
|
*/
|
|
@@ -41,8 +41,7 @@ describe("assemblePaper — the text TeX reads", () => {
|
|
|
41
41
|
main,
|
|
42
42
|
deps({ "s/one.tex": "one", "two.tex": "two" }),
|
|
43
43
|
);
|
|
44
|
-
expect(p.text).toBe("
|
|
45
|
-
expect(p.missing).toEqual([]);
|
|
44
|
+
expect(p.text).toBe("Aone\nBtwo\nC");
|
|
46
45
|
expect([...new Set(p.segments.map((g) => g.file))]).toEqual([
|
|
47
46
|
"paper.tex",
|
|
48
47
|
"s/one.tex",
|
|
@@ -57,7 +56,7 @@ describe("assemblePaper — the text TeX reads", () => {
|
|
|
57
56
|
main,
|
|
58
57
|
deps({ "a.tex": "[\\input{s/b}]", "s/b.tex": "BB" }),
|
|
59
58
|
);
|
|
60
|
-
expect(p.text).toBe("x[BB]
|
|
59
|
+
expect(p.text).toBe("x[BB\n]\ny");
|
|
61
60
|
expect(originOf(p, { start: 2, end: 4 })).toEqual({
|
|
62
61
|
file: "s/b.tex",
|
|
63
62
|
source: "BB",
|
|
@@ -66,18 +65,10 @@ describe("assemblePaper — the text TeX reads", () => {
|
|
|
66
65
|
});
|
|
67
66
|
});
|
|
68
67
|
|
|
69
|
-
it("
|
|
70
|
-
const
|
|
71
|
-
const p = assemblePaper("paper.tex", main, deps({}));
|
|
68
|
+
it("a file the reader has none of contributes nothing", () => {
|
|
69
|
+
const p = assemblePaper("paper.tex", "a\\input{gone}b", deps({}));
|
|
72
70
|
expect(p.text).toBe("ab");
|
|
73
|
-
expect(p.
|
|
74
|
-
{
|
|
75
|
-
file: "paper.tex",
|
|
76
|
-
target: "gone",
|
|
77
|
-
span: { start: 1, end: 13 },
|
|
78
|
-
via: { start: 1, end: 13 },
|
|
79
|
-
},
|
|
80
|
-
]);
|
|
71
|
+
expect(p.segments.map((g) => g.file)).toEqual(["paper.tex", "paper.tex"]);
|
|
81
72
|
});
|
|
82
73
|
});
|
|
83
74
|
|
|
@@ -99,7 +90,7 @@ describe("assemblePaper — subfiles and a file that includes itself", () => {
|
|
|
99
90
|
"<\\subfile{s}>",
|
|
100
91
|
deps({ "s.tex": "ALL" }),
|
|
101
92
|
);
|
|
102
|
-
expect(p.text).toBe("<ALL>");
|
|
93
|
+
expect(p.text).toBe("<ALL\n>");
|
|
103
94
|
});
|
|
104
95
|
|
|
105
96
|
it("a file that includes itself is entered once", () => {
|
|
@@ -108,7 +99,54 @@ describe("assemblePaper — subfiles and a file that includes itself", () => {
|
|
|
108
99
|
"m\\input{a}",
|
|
109
100
|
deps({ "a.tex": "a\\input{a}\\input{paper.tex}" }),
|
|
110
101
|
);
|
|
111
|
-
expect(p.text).toBe("ma");
|
|
102
|
+
expect(p.text).toBe("ma\n");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("assemblePaper — TeX's end of line after the last line of an included file", () => {
|
|
107
|
+
// Measured with TeX Live 2026: `a\input f b` with f = `foo`, no final newline, typesets «afoo b» —
|
|
108
|
+
// the file's last line is ended like any other; `f` = `bar` plus a newline typesets the same; an
|
|
109
|
+
// empty file reads no line and adds nothing.
|
|
110
|
+
it("a file whose last line has no newline is ended, so what follows it is a new word", () => {
|
|
111
|
+
const p = assemblePaper(
|
|
112
|
+
"paper.tex",
|
|
113
|
+
"a\\input{f}b",
|
|
114
|
+
deps({ "f.tex": "foo" }),
|
|
115
|
+
);
|
|
116
|
+
expect(p.text).toBe("afoo\nb");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a file that ends with a newline gets no second one", () => {
|
|
120
|
+
const p = assemblePaper(
|
|
121
|
+
"paper.tex",
|
|
122
|
+
"a\\input{f}b",
|
|
123
|
+
deps({ "f.tex": "foo\n" }),
|
|
124
|
+
);
|
|
125
|
+
expect(p.text).toBe("afoo\nb");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("an empty file reads no line: it adds nothing", () => {
|
|
129
|
+
const p = assemblePaper("paper.tex", "a\\input{f}b", deps({ "f.tex": "" }));
|
|
130
|
+
expect(p.text).toBe("ab");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("the main file is not ended: its text is the caller's, as given", () => {
|
|
134
|
+
const p = assemblePaper("paper.tex", "no newline", deps({}));
|
|
135
|
+
expect(p.text).toBe("no newline");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("the end of line belongs to the file it ends, at the end of that file", () => {
|
|
139
|
+
const p = assemblePaper(
|
|
140
|
+
"paper.tex",
|
|
141
|
+
"a\\input{f}b",
|
|
142
|
+
deps({ "f.tex": "foo" }),
|
|
143
|
+
);
|
|
144
|
+
expect(originOf(p, { start: 4, end: 5 })).toEqual({
|
|
145
|
+
file: "f.tex",
|
|
146
|
+
source: "foo",
|
|
147
|
+
span: { start: 3, end: 4 },
|
|
148
|
+
via: { start: 1, end: 10 },
|
|
149
|
+
});
|
|
112
150
|
});
|
|
113
151
|
});
|
|
114
152
|
|
|
@@ -129,7 +167,7 @@ describe("originOf — where a span of the assembled text comes from", () => {
|
|
|
129
167
|
});
|
|
130
168
|
|
|
131
169
|
it("after the include: offsets in the main file past the macro", () => {
|
|
132
|
-
expect(originOf(p, { start:
|
|
170
|
+
expect(originOf(p, { start: 6, end: 8 })).toEqual({
|
|
133
171
|
file: "paper.tex",
|
|
134
172
|
source: "ab\\input{x}cd",
|
|
135
173
|
span: { start: 11, end: 13 },
|
|
@@ -147,10 +185,10 @@ describe("originOf — where a span of the assembled text comes from", () => {
|
|
|
147
185
|
});
|
|
148
186
|
|
|
149
187
|
it("an empty span at the very end belongs to the last stretch; outside the text there is none", () => {
|
|
150
|
-
expect(originOf(p, { start:
|
|
188
|
+
expect(originOf(p, { start: 8, end: 8 })?.span).toEqual({
|
|
151
189
|
start: 13,
|
|
152
190
|
end: 13,
|
|
153
191
|
});
|
|
154
|
-
expect(originOf(p, { start:
|
|
192
|
+
expect(originOf(p, { start: 10, end: 10 })).toBe(null);
|
|
155
193
|
});
|
|
156
194
|
});
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
* answers about one file (the class, the outline, the rendered text, the body's passages) is
|
|
10
10
|
* answered about the paper, unchanged. A subfile is a document of its own; only its body is spliced.
|
|
11
11
|
*
|
|
12
|
+
* ── THE END OF AN INCLUDED FILE'S LAST LINE ─────────────────────────────────────
|
|
13
|
+
* TeX ends every line it reads with its end-of-line character, the last line of a file included:
|
|
14
|
+
* `a\input{f}b` with `f` holding `foo` and no final newline typesets "afoo b" (measured, TeX Live
|
|
15
|
+
* 2026), not "afoob". The splice does the same: an included file whose text does not end in a newline
|
|
16
|
+
* gets one, mapped to the end of that file; an empty file read no line and gets nothing. The main file
|
|
17
|
+
* is the caller's text and is left as given.
|
|
18
|
+
*
|
|
12
19
|
* ── WHAT IS NOT DONE ─────────────────────────────────────────────────────────────
|
|
13
20
|
* No macro is expanded, so a path built from a macro (`\input{\dir/intro}`) is read as written. A
|
|
14
|
-
* file
|
|
15
|
-
*
|
|
21
|
+
* file the caller's `read` has none of contributes nothing; a file that includes itself, directly or
|
|
22
|
+
* through others, is spliced once.
|
|
16
23
|
*/
|
|
17
|
-
import {
|
|
24
|
+
import { posix } from "node:path";
|
|
18
25
|
import type { Span } from "./tex-document.ts";
|
|
19
26
|
|
|
20
27
|
/** One `\input`, `\include` or `\subfile` in a source: the path as written and where the macro stands. */
|
|
@@ -46,27 +53,17 @@ export interface Segment {
|
|
|
46
53
|
readonly via: Span | null;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
/**
|
|
50
|
-
export interface MissingInclude {
|
|
51
|
-
readonly file: string;
|
|
52
|
-
readonly target: string;
|
|
53
|
-
readonly span: Span;
|
|
54
|
-
/** Where it stands in the main file (the include itself, or the one that brought its file in). */
|
|
55
|
-
readonly via: Span;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** The paper as one text, the map back to its files, and the includes that were not found. */
|
|
56
|
+
/** The paper as one text, and the map back to its files. */
|
|
59
57
|
export interface PaperSource {
|
|
60
58
|
readonly main: string;
|
|
61
59
|
readonly text: string;
|
|
62
60
|
readonly segments: readonly Segment[];
|
|
63
|
-
readonly missing: readonly MissingInclude[];
|
|
64
61
|
}
|
|
65
62
|
|
|
66
63
|
/** The paths TeX tries for a target: `name.tex`, then `name` — or the name alone if it has one. */
|
|
67
64
|
const candidates = (target: string): readonly string[] => {
|
|
68
|
-
const t = normalize(target.trim());
|
|
69
|
-
return extname(t) === "" ? [`${t}.tex`, t] : [t];
|
|
65
|
+
const t = posix.normalize(target.trim());
|
|
66
|
+
return posix.extname(t) === "" ? [`${t}.tex`, t] : [t];
|
|
70
67
|
};
|
|
71
68
|
|
|
72
69
|
/** The first candidate path that exists, with its text. */
|
|
@@ -81,11 +78,10 @@ function found(
|
|
|
81
78
|
return hits[0] ?? null;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
/** An assembled piece of text: the text
|
|
81
|
+
/** An assembled piece of text: the text and its segments from 0. */
|
|
85
82
|
interface Piece {
|
|
86
83
|
readonly text: string;
|
|
87
84
|
readonly segments: readonly Segment[];
|
|
88
|
-
readonly missing: readonly MissingInclude[];
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
/** `segments` moved by `by` characters. */
|
|
@@ -111,11 +107,10 @@ const copied = ({ file, text, from, to, via }: Stretch): Piece => ({
|
|
|
111
107
|
to > from
|
|
112
108
|
? [{ start: 0, end: to - from, file, source: text, from, via }]
|
|
113
109
|
: [],
|
|
114
|
-
missing: [],
|
|
115
110
|
});
|
|
116
111
|
|
|
117
112
|
/** Nothing: an include left out. */
|
|
118
|
-
const NOTHING: Piece = { text: "", segments: []
|
|
113
|
+
const NOTHING: Piece = { text: "", segments: [] };
|
|
119
114
|
|
|
120
115
|
/** Pieces joined in order. */
|
|
121
116
|
const joined = (pieces: readonly Piece[]): Piece =>
|
|
@@ -123,7 +118,6 @@ const joined = (pieces: readonly Piece[]): Piece =>
|
|
|
123
118
|
(acc, p) => ({
|
|
124
119
|
text: acc.text + p.text,
|
|
125
120
|
segments: [...acc.segments, ...shifted(p.segments, acc.text.length)],
|
|
126
|
-
missing: [...acc.missing, ...p.missing],
|
|
127
121
|
}),
|
|
128
122
|
NOTHING,
|
|
129
123
|
);
|
|
@@ -153,7 +147,7 @@ function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
|
|
|
153
147
|
pieces: [
|
|
154
148
|
...acc.pieces,
|
|
155
149
|
at(acc.cursor, inc.span.start),
|
|
156
|
-
included(
|
|
150
|
+
included(inc, ctx),
|
|
157
151
|
],
|
|
158
152
|
cursor: inc.span.end,
|
|
159
153
|
}),
|
|
@@ -162,23 +156,30 @@ function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
|
|
|
162
156
|
return joined([...pieces, at(cursor, range.end)]);
|
|
163
157
|
}
|
|
164
158
|
|
|
165
|
-
/**
|
|
166
|
-
|
|
159
|
+
/** TeX's end of line after the last line of `file`, standing where its text ends: one newline. */
|
|
160
|
+
const endOfLine = (file: string, text: string, via: Span | null): Piece => ({
|
|
161
|
+
text: "\n",
|
|
162
|
+
segments: [{ start: 0, end: 1, file, source: text, from: text.length, via }],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/** One include: the file it names, spliced, or — when the reader has none of it — nothing. */
|
|
166
|
+
function included(inc: Include, ctx: Context): Piece {
|
|
167
167
|
const via = ctx.via ?? inc.span;
|
|
168
168
|
const hit = found(inc.target, ctx.deps.read);
|
|
169
|
-
if (hit === null)
|
|
170
|
-
return {
|
|
171
|
-
...NOTHING,
|
|
172
|
-
missing: [{ file, target: inc.target, span: inc.span, via }],
|
|
173
|
-
};
|
|
174
|
-
if (ctx.stack.includes(hit.path)) return NOTHING;
|
|
169
|
+
if (hit === null || ctx.stack.includes(hit.path)) return NOTHING;
|
|
175
170
|
const body = inc.macro === "subfile" ? ctx.deps.documentBody(hit.text) : null;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
);
|
|
171
|
+
const range = body ?? { start: 0, end: hit.text.length };
|
|
172
|
+
const piece = spliced(hit.path, hit.text, range, {
|
|
173
|
+
deps: ctx.deps,
|
|
174
|
+
stack: [...ctx.stack, hit.path],
|
|
175
|
+
via,
|
|
176
|
+
});
|
|
177
|
+
// The last line of the file is read to its end; a subfile's body ends inside its last line.
|
|
178
|
+
const endsLine =
|
|
179
|
+
range.end === hit.text.length &&
|
|
180
|
+
hit.text !== "" &&
|
|
181
|
+
!hit.text.endsWith("\n");
|
|
182
|
+
return endsLine ? joined([piece, endOfLine(hit.path, hit.text, via)]) : piece;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
/** The paper whose main file is `main` (a path relative to the paper directory) with `text`. */
|
|
@@ -193,12 +194,7 @@ export function assemblePaper(
|
|
|
193
194
|
{ start: 0, end: text.length },
|
|
194
195
|
{ deps, stack: [main], via: null },
|
|
195
196
|
);
|
|
196
|
-
return {
|
|
197
|
-
main,
|
|
198
|
-
text: p.text,
|
|
199
|
-
segments: p.segments,
|
|
200
|
-
missing: p.missing,
|
|
201
|
-
};
|
|
197
|
+
return { main, text: p.text, segments: p.segments };
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
/** Where a span of the assembled text stands: in which file, at which offsets, and via which include. */
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A DATABASE IS — the values the bibtex reader (`BibReader`, src/ports/bib-reader.ts) returns for
|
|
3
|
+
* a `.bib`, or for the body of a `filecontents` block: its entries, and the bytes bibtex takes from
|
|
4
|
+
* them. Which databases a paper reads is TeX's answer, recorded by the build (`_build/sources.json`,
|
|
5
|
+
* src/domain/sources-record.ts, docs/design/paper-sources.md §1); nothing here decides it.
|
|
6
|
+
*/
|
|
7
|
+
import type { AbsolutePath } from "./paths.ts";
|
|
8
|
+
import type { Span } from "./tex-document.ts";
|
|
9
|
+
|
|
10
|
+
/** The main file's name — the file TeX is given. */
|
|
11
|
+
export const MAIN_FILE = "paper.tex";
|
|
12
|
+
|
|
13
|
+
/** A name in a name list (`author`, `editor`), split into its parts as BibTeX defines them. */
|
|
14
|
+
export interface BibName {
|
|
15
|
+
readonly lastName?: string;
|
|
16
|
+
readonly firstName?: string;
|
|
17
|
+
readonly prefix?: string;
|
|
18
|
+
readonly suffix?: string;
|
|
19
|
+
/** A name written as one unit (`{World Health Organization}`). */
|
|
20
|
+
readonly name?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The `@string`s in force: each name as bibtex folds it (upper case) → its value as bibtex takes it,
|
|
25
|
+
* the `@string`s it used expanded and its LaTeX as written. bibtex reads a paper's databases one after
|
|
26
|
+
* the other with one table of them, so a `@string` an earlier database defines is in force in a later
|
|
27
|
+
* one (measured: `\\bibliography{abbreviations,references}` typesets an entry of `references.bib` with
|
|
28
|
+
* the author and booktitle `abbreviations.bib` defines; in the other order both are undefined).
|
|
29
|
+
*/
|
|
30
|
+
export type BibStrings = Readonly<Record<string, string>>;
|
|
31
|
+
|
|
32
|
+
/** One entry of a database. */
|
|
33
|
+
export interface BibEntry {
|
|
34
|
+
/** Lower-cased (`@Article` → `article`). */
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly key: string;
|
|
37
|
+
/** From its `@` to its closing brace or parenthesis, in the text of the file that holds it. */
|
|
38
|
+
readonly span: Span;
|
|
39
|
+
/** Its fields, LaTeX read into text (`{\"u}` → `ü`, `\url{x}` → a link); a list field joined by ` and `. */
|
|
40
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
41
|
+
/** Its name lists (`author`, `editor`, …), each name split into its parts. */
|
|
42
|
+
readonly names: Readonly<Record<string, readonly BibName[]>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The bytes of one database, where they sit, and what the reader read from them. */
|
|
46
|
+
export interface BibText {
|
|
47
|
+
/** The file holding the bytes: the `.bib`, or the `.tex` that holds the block. */
|
|
48
|
+
readonly path: AbsolutePath;
|
|
49
|
+
/** That file's whole text; every span here is an offset into it. */
|
|
50
|
+
readonly text: string;
|
|
51
|
+
/** The database inside `text`: all of it for a `.bib`, the body for a block. */
|
|
52
|
+
readonly body: Span;
|
|
53
|
+
readonly entries: readonly BibEntry[];
|
|
54
|
+
/** The same entries as bibtex takes them, in order: what two databases are compared by. */
|
|
55
|
+
readonly written: readonly WrittenEntry[];
|
|
56
|
+
/** The `@preamble` commands, as written, in order: bibtex writes them into the `.bbl`. */
|
|
57
|
+
readonly preamble: readonly string[];
|
|
58
|
+
/** The `@string`s the databases bibtex read before this one left in force: none for one read alone. */
|
|
59
|
+
readonly inherited: BibStrings;
|
|
60
|
+
/** The `@string`s in force after it — `inherited` and its own, a later one winning: what the next database starts with. */
|
|
61
|
+
readonly strings: BibStrings;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* An entry as bibtex takes it from the database: the type, the key, and each field's value with the
|
|
66
|
+
* `@string`s it uses expanded where it uses them (a later redefinition changes nothing) and its LaTeX
|
|
67
|
+
* as written — `{\"o}` and `ö` are different values here, as they are in the `.bbl` bibtex writes.
|
|
68
|
+
*/
|
|
69
|
+
export interface WrittenEntry {
|
|
70
|
+
/** Lower-cased, as bibtex reads it. */
|
|
71
|
+
readonly type: string;
|
|
72
|
+
readonly key: string;
|
|
73
|
+
/** Whitespace runs folded to one space, as bibtex folds them. */
|
|
74
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What bibtex takes from a database, for comparing two: each entry as bibtex reads it, in order, its
|
|
79
|
+
* fields in name order, and the `@preamble` commands. Nothing else in the bytes reaches the `.bbl`:
|
|
80
|
+
* not the text between entries (a `filecontents` header among it), not a `@string` no entry uses.
|
|
81
|
+
*/
|
|
82
|
+
const contentOf = (b: BibText): string =>
|
|
83
|
+
JSON.stringify([
|
|
84
|
+
b.written.map((e) => [
|
|
85
|
+
e.type,
|
|
86
|
+
e.key,
|
|
87
|
+
Object.entries(e.fields).sort(([x], [y]) => x.localeCompare(y)),
|
|
88
|
+
]),
|
|
89
|
+
b.preamble,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/** Whether two databases hold the same, as TeX's own copy of a block holds the block's. */
|
|
93
|
+
export const sameDatabase = (a: BibText, b: BibText): boolean =>
|
|
94
|
+
contentOf(a) === contentOf(b);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record of a build as a value: the one text it is parsed from, what a rule reads off it, and what
|
|
3
|
+
* changed since it was made. The files it is about are `src/sources-record.test.ts`'s.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { parseSha256 } from "./sha256.ts";
|
|
7
|
+
import {
|
|
8
|
+
changesSince,
|
|
9
|
+
describeChanges,
|
|
10
|
+
serializeSourcesRecord,
|
|
11
|
+
type SourcesRecord,
|
|
12
|
+
} from "./sources-record.ts";
|
|
13
|
+
|
|
14
|
+
const A = "a".repeat(64);
|
|
15
|
+
const B = "b".repeat(64);
|
|
16
|
+
|
|
17
|
+
const RECORD: SourcesRecord = {
|
|
18
|
+
schema: 1,
|
|
19
|
+
inputs: [
|
|
20
|
+
{ path: "paper.tex", role: "body" },
|
|
21
|
+
{ path: "macros.tex", role: "preamble" },
|
|
22
|
+
{ path: "sections/intro.tex", role: "body" },
|
|
23
|
+
],
|
|
24
|
+
written: ["refs.bib"],
|
|
25
|
+
bibdata: ["refs"],
|
|
26
|
+
bibtex: {
|
|
27
|
+
ran: true,
|
|
28
|
+
databases: ["refs.bib"],
|
|
29
|
+
keys: ["k1"],
|
|
30
|
+
exit: 2,
|
|
31
|
+
errors: [{ message: "bad", file: "refs.bib", line: 4 }],
|
|
32
|
+
},
|
|
33
|
+
sha256: { "paper.tex": A, "macros.tex": null },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("serializing a record", () => {
|
|
37
|
+
it("is two-space JSON with a final newline, so a diff of two builds reads", () => {
|
|
38
|
+
expect(serializeSourcesRecord({ ...RECORD, bibtex: { ran: false } })).toBe(
|
|
39
|
+
`${JSON.stringify({ ...RECORD, bibtex: { ran: false } }, null, 2)}\n`,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("what changed since the record", () => {
|
|
45
|
+
const now = (files: Readonly<Record<string, string>>) => (p: string) =>
|
|
46
|
+
files[p] === undefined ? null : parseSha256(files[p]);
|
|
47
|
+
|
|
48
|
+
it("a record whose files are as hashed has no change, a file absent then and now included", () => {
|
|
49
|
+
expect(changesSince(RECORD, now({ "paper.tex": A }))).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("an edited file, a deleted one, and a file that appeared where there was none", () => {
|
|
53
|
+
expect(changesSince(RECORD, now({ "macros.tex": B }))).toEqual([
|
|
54
|
+
{ path: "paper.tex", change: "deleted" },
|
|
55
|
+
{ path: "macros.tex", change: "added" },
|
|
56
|
+
]);
|
|
57
|
+
expect(changesSince(RECORD, now({ "paper.tex": B }))).toEqual([
|
|
58
|
+
{ path: "paper.tex", change: "edited" },
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("names the changes in a line a finding can carry", () => {
|
|
63
|
+
expect(
|
|
64
|
+
describeChanges([
|
|
65
|
+
{ path: "paper.tex", change: "edited" },
|
|
66
|
+
{ path: "sections/a.tex", change: "deleted" },
|
|
67
|
+
]),
|
|
68
|
+
).toBe("paper.tex edited, sections/a.tex deleted");
|
|
69
|
+
expect(describeChanges([])).toBe("");
|
|
70
|
+
});
|
|
71
|
+
});
|