paperlint 8.0.0 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -531
- package/CONTRIBUTING.md +7 -6
- package/README.md +4 -3
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- 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 +41 -25
- 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/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- 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/eslint-rules/pdf-last-page-balance.js +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/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.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-requirements.js +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/dist/toolchain.js +1 -1
- package/docs/configuration.md +12 -4
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +1 -1
- package/docs/incidents.md +5 -5
- 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/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- 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-rules/pdf-last-page-balance.ts +1 -1
- package/eslint.config.mjs +28 -3
- 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/hooks/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/package.json +3 -11
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +21 -11
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- 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/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- 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/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- 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 +101 -36
- 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-requirements.ts +1 -1
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/src/toolchain.ts +1 -1
- 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/heading-case.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { run } from "./cli.ts";
|
|
|
12
12
|
import { headingCaseRule } from "./heading-case.ts";
|
|
13
13
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
14
14
|
import { useTempDir, writeTree } from "../test/support.ts";
|
|
15
|
+
import { recordedTree } from "../test/recorded-fixture.ts";
|
|
15
16
|
|
|
16
17
|
const Results = z.array(
|
|
17
18
|
z.looseObject({
|
|
@@ -369,10 +370,15 @@ describe("tex/heading-case refuses, when ESLint loads it, a configuration that a
|
|
|
369
370
|
});
|
|
370
371
|
|
|
371
372
|
describe("tex/heading-case in the files a paper includes", () => {
|
|
372
|
-
const tree =
|
|
373
|
-
doc("\\section{Introduction}\n\\input{sections/results}"),
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
const tree = recordedTree(
|
|
374
|
+
paper(doc("\\section{Introduction}\n\\input{sections/results}"), PRESET, {
|
|
375
|
+
"papers/p/sections/results.tex": "\\section{Results and discussion}\n",
|
|
376
|
+
}),
|
|
377
|
+
"papers/p",
|
|
378
|
+
[
|
|
379
|
+
{ path: "paper.tex", role: "body" },
|
|
380
|
+
{ path: "sections/results.tex", role: "body" },
|
|
381
|
+
],
|
|
376
382
|
);
|
|
377
383
|
|
|
378
384
|
it("reports a heading at its own file and line, and --fix edits that file", async () => {
|
|
@@ -391,12 +397,20 @@ describe("tex/heading-case in the files a paper includes", () => {
|
|
|
391
397
|
});
|
|
392
398
|
|
|
393
399
|
it("🔴 reads a \\title kept in a file the body includes, where it stands", async () => {
|
|
394
|
-
const withTitle =
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
+
const withTitle = recordedTree(
|
|
401
|
+
paper(
|
|
402
|
+
`${ACM}\n\\begin{document}\n\\input{front}\n\\maketitle\n\\end{document}\n`,
|
|
403
|
+
PRESET,
|
|
404
|
+
{
|
|
405
|
+
"papers/p/front.tex":
|
|
406
|
+
"% the front matter\n\\title{Measuring things}\n",
|
|
407
|
+
},
|
|
408
|
+
),
|
|
409
|
+
"papers/p",
|
|
410
|
+
[
|
|
411
|
+
{ path: "paper.tex", role: "body" },
|
|
412
|
+
{ path: "front.tex", role: "body" },
|
|
413
|
+
],
|
|
400
414
|
);
|
|
401
415
|
const { found } = await lint(withTitle);
|
|
402
416
|
expect(found).toEqual([
|
package/src/latex-log.ts
CHANGED
|
@@ -146,6 +146,26 @@ export function bibtexExcerpt(stdout: string): string[] {
|
|
|
146
146
|
.slice(0, MAX_EXCERPT);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** What a bibtex run read: the databases its `.blg` names, the entries its `.bbl` holds. */
|
|
150
|
+
export interface BibtexRead {
|
|
151
|
+
readonly databases: readonly string[];
|
|
152
|
+
readonly bibitems: readonly string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `\bibitem{key}`, `\bibitem[label]{key}` — a label may hold one level of braces (natbib's `[{A}(2020)]`). */
|
|
156
|
+
const BIBITEM = /\\bibitem\s*(?:\[(?:[^[\]{}]|\{[^{}]*\})*\])?\s*\{([^}]+)\}/g;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* What a bibtex run read, from the files it left: `Database file #n: name` in its `.blg` (the name
|
|
160
|
+
* as the `.aux` gave it, relative to where bibtex ran), and every key its `.bbl` typesets.
|
|
161
|
+
*/
|
|
162
|
+
export const bibtexRead = (blg: string, bbl: string): BibtexRead => ({
|
|
163
|
+
databases: [...blg.matchAll(/^Database file #\d+: (.+)$/gm)].flatMap((m) =>
|
|
164
|
+
m.slice(1, 2),
|
|
165
|
+
),
|
|
166
|
+
bibitems: [...bbl.matchAll(BIBITEM)].flatMap((m) => m.slice(1, 2)),
|
|
167
|
+
});
|
|
168
|
+
|
|
149
169
|
/** What the `.aux` tells bibtex: the cited keys, the databases and the style. */
|
|
150
170
|
export interface AuxBib {
|
|
151
171
|
/** Sorted, unique. `*` (`\nocite{*}`) is a key like any other. */
|
package/src/latex-loop.ts
CHANGED
|
@@ -86,6 +86,8 @@ export type Observation =
|
|
|
86
86
|
readonly bib: BibInput;
|
|
87
87
|
/** The first error and its source context, from the log. Empty on success. */
|
|
88
88
|
readonly errorLines: readonly string[];
|
|
89
|
+
/** What `-recorder` wrote after this pass (`paper.fls`), or null when pdflatex wrote none. */
|
|
90
|
+
readonly fls: string | null;
|
|
89
91
|
}
|
|
90
92
|
| {
|
|
91
93
|
readonly step: "bibtex";
|
package/src/linted-set.test.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { join, relative } from "node:path";
|
|
|
20
20
|
import { afterEach, describe, expect, it } from "vitest";
|
|
21
21
|
import { run } from "./cli.ts";
|
|
22
22
|
import { lintReport } from "../test/lint-report.ts";
|
|
23
|
+
import { recordedTree } from "../test/recorded-fixture.ts";
|
|
23
24
|
|
|
24
25
|
const dirs: string[] = [];
|
|
25
26
|
afterEach(() => {
|
|
@@ -87,7 +88,14 @@ function project(): string {
|
|
|
87
88
|
"papers/a/sections/intro.tex": "Intro.\n",
|
|
88
89
|
...NEIGHBOURS,
|
|
89
90
|
};
|
|
90
|
-
|
|
91
|
+
// As a build left it: TeX read paper.tex, the macro file in the preamble and the section in the body
|
|
92
|
+
// (`paper-guards` is paperlint's own input, outside the paper's directory, so the record has none).
|
|
93
|
+
const built = recordedTree(all, "papers/a", [
|
|
94
|
+
{ path: "paper.tex", role: "body" },
|
|
95
|
+
{ path: "macros.tex", role: "preamble" },
|
|
96
|
+
{ path: "sections/intro.tex", role: "body" },
|
|
97
|
+
]);
|
|
98
|
+
for (const [p, text] of Object.entries(built)) {
|
|
91
99
|
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
92
100
|
writeFileSync(join(root, p), text);
|
|
93
101
|
}
|
|
@@ -112,10 +112,11 @@ try {
|
|
|
112
112
|
);
|
|
113
113
|
const l = await lint(join(papers, "demo"));
|
|
114
114
|
check(
|
|
115
|
-
"🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with
|
|
115
|
+
"🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with two warnings: no venue is chosen yet, and the paper is not built",
|
|
116
116
|
l.code === 0 &&
|
|
117
117
|
/names no venue preset yet/.test(l.text) &&
|
|
118
|
-
/
|
|
118
|
+
/paper\/sources-fresh/.test(l.text) &&
|
|
119
|
+
/2 problems \(0 errors, 2 warnings\)/.test(l.text),
|
|
119
120
|
);
|
|
120
121
|
|
|
121
122
|
// What the status hook reads on every paper edit: the verdict line.
|
package/src/new-paper.test.ts
CHANGED
|
@@ -122,10 +122,13 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
122
122
|
return { code, messages };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
it("gets
|
|
125
|
+
it("gets the warning naming the file to set, besides the one that it is not built, and exits 0", async () => {
|
|
126
126
|
const { code, messages } = await lintNew(null);
|
|
127
127
|
expect(code).toBe(0);
|
|
128
|
-
expect(messages).
|
|
128
|
+
expect(messages.map((m) => m.ruleId)).toEqual([
|
|
129
|
+
"pdf/measured",
|
|
130
|
+
"paper/sources-fresh",
|
|
131
|
+
]);
|
|
129
132
|
expect(messages[0]?.ruleId).toBe("pdf/measured");
|
|
130
133
|
expect(messages[0]?.severity).toBe(1);
|
|
131
134
|
expect(messages[0]?.message).toMatch(/names no venue preset yet/);
|
|
@@ -254,10 +257,11 @@ describe("paperlint new --venue msr", () => {
|
|
|
254
257
|
log: (s: string) => out.push(s),
|
|
255
258
|
err: () => {},
|
|
256
259
|
});
|
|
257
|
-
// Guards: what remains is the reminder that the
|
|
258
|
-
// about the paper.
|
|
259
|
-
expect(out.join("\n")).toMatch(/\(0 errors,
|
|
260
|
+
// Guards: what remains is the reminder that the paper is not built yet — two warnings, not a
|
|
261
|
+
// finding about the paper: no PDF measured, no record of what TeX read.
|
|
262
|
+
expect(out.join("\n")).toMatch(/\(0 errors, 2 warnings\)/);
|
|
260
263
|
expect(out.join("\n")).toMatch(/pdf\/measured/);
|
|
264
|
+
expect(out.join("\n")).toMatch(/paper\/sources-fresh/);
|
|
261
265
|
expect(code).toBe(0);
|
|
262
266
|
});
|
|
263
267
|
});
|
package/src/package-dirs.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* presets/ the venue presets (`paperlint:<name>`) and their JSON Schema
|
|
7
7
|
* presets/tex/ the LaTeX inputs `paperlint build` puts on TEXINPUTS
|
|
8
8
|
*
|
|
9
|
-
* `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX
|
|
10
|
-
*
|
|
9
|
+
* `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX; lint does
|
|
10
|
+
* not search for an include at all — it reads the files TeX read, from the build's record
|
|
11
|
+
* (docs/design/paper-sources.md §1).
|
|
11
12
|
*/
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `paperlint lint` over a paper whose body is in other files: the rules over ESLint's LaTeX text read
|
|
3
|
-
* every file
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* every body file the last build's record lists (`_build/sources.json`: the files TeX read after
|
|
4
|
+
* `\\begin{document}`), report at that file's own path and line, and fix that file. A paper with no
|
|
5
|
+
* record, or one it has outgrown, is `paper.tex` alone and `paper/sources-fresh` says the rest went
|
|
6
|
+
* unlinted. What a rule decides for the whole paper — the majority reference form and whether the
|
|
7
|
+
* build is in review mode — it decides over all of the paper's files, not over the one it is
|
|
8
|
+
* reporting in.
|
|
7
9
|
*/
|
|
8
10
|
import {
|
|
9
11
|
mkdirSync,
|
|
@@ -16,8 +18,13 @@ import {
|
|
|
16
18
|
import { tmpdir } from "node:os";
|
|
17
19
|
import { join, relative } from "node:path";
|
|
18
20
|
import { afterEach, describe, expect, it } from "vitest";
|
|
21
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
22
|
+
import { absolutePath } from "./domain/paths.ts";
|
|
23
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
19
24
|
import { run } from "./cli.ts";
|
|
25
|
+
import { includeBlocks, paperBodies } from "./paper-includes.ts";
|
|
20
26
|
import { lintReport } from "../test/lint-report.ts";
|
|
27
|
+
import { builtFixture, recordedTree } from "../test/recorded-fixture.ts";
|
|
21
28
|
|
|
22
29
|
const dirs: string[] = [];
|
|
23
30
|
afterEach(() => {
|
|
@@ -29,7 +36,21 @@ const doc = (cls: string, preamble: string, body: string): string =>
|
|
|
29
36
|
|
|
30
37
|
const INTRO = "See §3 and p < .05.\nAs Fig.~\\ref{f3} shows.\n";
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
/** What TeX read of the paper `project` makes, in the order it read it. */
|
|
40
|
+
const READ = [
|
|
41
|
+
{ path: "paper.tex", role: "body" },
|
|
42
|
+
{ path: "macros.tex", role: "preamble" },
|
|
43
|
+
{ path: "sections/intro.tex", role: "body" },
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A project with one paper, `papers/a`, as a build left it: its files, and a record listing `inputs`.
|
|
48
|
+
* `inputs: null` is a paper nothing has built.
|
|
49
|
+
*/
|
|
50
|
+
function project(
|
|
51
|
+
files: Record<string, string>,
|
|
52
|
+
inputs: readonly { path: string; role: "preamble" | "body" }[] | null = READ,
|
|
53
|
+
): string {
|
|
33
54
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-inc-")));
|
|
34
55
|
dirs.push(root);
|
|
35
56
|
const all: Record<string, string> = {
|
|
@@ -51,7 +72,8 @@ function project(files: Record<string, string>): string {
|
|
|
51
72
|
),
|
|
52
73
|
...files,
|
|
53
74
|
};
|
|
54
|
-
|
|
75
|
+
const written = inputs === null ? all : recordedTree(all, "papers/a", inputs);
|
|
76
|
+
for (const [p, text] of Object.entries(written)) {
|
|
55
77
|
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
56
78
|
writeFileSync(join(root, p), text);
|
|
57
79
|
}
|
|
@@ -62,7 +84,11 @@ function project(files: Record<string, string>): string {
|
|
|
62
84
|
async function lint(
|
|
63
85
|
root: string,
|
|
64
86
|
args: string[] = [],
|
|
65
|
-
): Promise<{
|
|
87
|
+
): Promise<{
|
|
88
|
+
byFile: Record<string, string[]>;
|
|
89
|
+
said: Record<string, string[]>;
|
|
90
|
+
err: string;
|
|
91
|
+
}> {
|
|
66
92
|
const out: string[] = [];
|
|
67
93
|
const err: string[] = [];
|
|
68
94
|
await run(["lint", "--json", ...args], {
|
|
@@ -70,13 +96,20 @@ async function lint(
|
|
|
70
96
|
log: (s: string) => out.push(s),
|
|
71
97
|
err: (s: string) => err.push(s),
|
|
72
98
|
});
|
|
99
|
+
const report = lintReport(out.join("\n"));
|
|
73
100
|
const byFile = Object.fromEntries(
|
|
74
|
-
|
|
101
|
+
report.map((r) => [
|
|
75
102
|
relative(root, r.filePath),
|
|
76
103
|
r.messages.map((m) => m.ruleId ?? "(fatal)").sort(),
|
|
77
104
|
]),
|
|
78
105
|
);
|
|
79
|
-
|
|
106
|
+
const said = Object.fromEntries(
|
|
107
|
+
report.map((r) => [
|
|
108
|
+
relative(root, r.filePath),
|
|
109
|
+
r.messages.map((m) => m.message),
|
|
110
|
+
]),
|
|
111
|
+
);
|
|
112
|
+
return { byFile, said, err: err.join("\n") };
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
describe("lint reads the files paper.tex includes from its body", () => {
|
|
@@ -104,18 +137,24 @@ describe("lint reads the files paper.tex includes from its body", () => {
|
|
|
104
137
|
|
|
105
138
|
it("🔴 an included file that is not `.tex` — a `.bbl` — is not handed to ESLint, which parsed it as JavaScript (#153)", async () => {
|
|
106
139
|
const { byFile, err } = await lint(
|
|
107
|
-
project(
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
project(
|
|
141
|
+
{
|
|
142
|
+
"papers/a/paper.tex": doc(
|
|
143
|
+
"{article}",
|
|
144
|
+
"",
|
|
145
|
+
"Text, as Figure~\\ref{f1} shows.\n\\input{paper.bbl}",
|
|
146
|
+
),
|
|
147
|
+
// What bibtex writes: a comment first, which ESLint's JavaScript parser stops at.
|
|
148
|
+
"papers/a/paper.bbl":
|
|
149
|
+
"% Generated by IEEEtran.bst\n\\begin{thebibliography}{1}\n" +
|
|
150
|
+
"\\bibitem{a} A. Author, ``A result with p < .05,'' 2024.\n" +
|
|
151
|
+
"\\end{thebibliography}\n",
|
|
152
|
+
},
|
|
153
|
+
[
|
|
154
|
+
{ path: "paper.tex", role: "body" },
|
|
155
|
+
{ path: "paper.bbl", role: "body" },
|
|
156
|
+
],
|
|
157
|
+
),
|
|
119
158
|
);
|
|
120
159
|
// Guards: the `.bbl` was the one file of the run with a parse error («Unexpected token %»).
|
|
121
160
|
// It is bibtex's output, not prose the author edits: no fragment rule reports in it, and
|
|
@@ -124,7 +163,6 @@ describe("lint reads the files paper.tex includes from its body", () => {
|
|
|
124
163
|
"papers/a/PIPELINE-STATUS.md": [],
|
|
125
164
|
"papers/a/paper.tex": [],
|
|
126
165
|
});
|
|
127
|
-
// Read, not missing: no «lint did not read» note names it.
|
|
128
166
|
expect(err).not.toMatch(/paper\.bbl/);
|
|
129
167
|
});
|
|
130
168
|
|
|
@@ -162,26 +200,107 @@ describe("what lint decides for the whole paper, it decides over every file of i
|
|
|
162
200
|
);
|
|
163
201
|
expect(review.byFile["papers/a/sections/intro.tex"]).toEqual([]);
|
|
164
202
|
});
|
|
203
|
+
});
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
205
|
+
describe("a built paper whose files are clean", () => {
|
|
206
|
+
it("is reported as the files that were checked — the paper's, and the section TeX read", async () => {
|
|
207
|
+
const root = project({
|
|
208
|
+
"papers/a/sections/intro.tex": "Plain words, nothing to flag.\n",
|
|
209
|
+
});
|
|
210
|
+
const out: string[] = [];
|
|
211
|
+
const code = await run(["lint"], {
|
|
212
|
+
cwd: root,
|
|
213
|
+
log: (s: string) => out.push(s),
|
|
214
|
+
err: () => undefined,
|
|
215
|
+
});
|
|
216
|
+
expect({ code, out }).toEqual({
|
|
217
|
+
code: 0,
|
|
218
|
+
out: ["✓ 3 file(s) checked, no findings"],
|
|
219
|
+
});
|
|
175
220
|
});
|
|
221
|
+
});
|
|
176
222
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
223
|
+
describe("a paper whose record is missing, stale or silent about a file", () => {
|
|
224
|
+
it("🔴 a paper no build has recorded is paper.tex alone: its includes are not linted, and paper/sources-fresh says so", async () => {
|
|
225
|
+
const { byFile } = await lint(project({}, null));
|
|
226
|
+
expect(byFile).toEqual({
|
|
227
|
+
"papers/a/PIPELINE-STATUS.md": [],
|
|
228
|
+
"papers/a/paper.tex": ["paper/sources-fresh"],
|
|
181
229
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("🔴 a paper changed since its build is paper.tex alone, and paper/sources-fresh names the file", async () => {
|
|
233
|
+
const root = project({});
|
|
234
|
+
writeFileSync(
|
|
235
|
+
join(root, "papers/a/sections/intro.tex"),
|
|
236
|
+
"Edited after the build, p < .05.\n",
|
|
237
|
+
);
|
|
238
|
+
const { byFile, said } = await lint(root);
|
|
239
|
+
expect(Object.keys(byFile)).not.toContain("papers/a/sections/intro.tex");
|
|
240
|
+
expect(said["papers/a/paper.tex"]).toEqual([
|
|
241
|
+
expect.stringMatching(/sections\/intro\.tex edited/),
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("paperBodies — the body files of the record TeX's own build left", () => {
|
|
247
|
+
const bodies = (files: Record<string, string>, dir = "/papers/p") =>
|
|
248
|
+
paperBodies([dir], { files: memoryFiles(files), codec: sourcesCodec });
|
|
249
|
+
|
|
250
|
+
it("p1: the one file TeX read after \\begin{document} — not the `.bib`, not the files its text names behind a comment or \\iffalse", () => {
|
|
251
|
+
expect(bodies(builtFixture("p1", "/papers/p"))).toEqual([
|
|
252
|
+
{
|
|
253
|
+
dir: "/papers/p",
|
|
254
|
+
main: "/papers/p/paper.tex",
|
|
255
|
+
files: ["/papers/p/sections/intro.tex"],
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("a body file that is a `.tex` listed after paper.tex is handed over; a preamble one is not (v16: a block written from a preamble include)", () => {
|
|
261
|
+
expect(
|
|
262
|
+
bodies(builtFixture("v16-included-block", "/papers/p"))[0]?.files,
|
|
263
|
+
).toEqual([]);
|
|
264
|
+
expect(
|
|
265
|
+
bodies(builtFixture("v17-include-order", "/papers/p"))[0]?.files,
|
|
266
|
+
).toEqual(["/papers/p/bibsetup.tex"]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("🔴 a body .tex TeX wrote from a filecontents block is not handed to ESLint: its text is the block's, and a fix there is overwritten or splits it from the block", () => {
|
|
270
|
+
const files = builtFixture("v29-generated-input", "/papers/p");
|
|
271
|
+
expect(files["/papers/p/body.tex"]).toBeDefined();
|
|
272
|
+
expect(bodies(files)[0]?.files).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("a directory without paper.tex is no paper", () => {
|
|
276
|
+
expect(bodies({}, "/papers/none")).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("includeBlocks — what ESLint is told of each paper", () => {
|
|
281
|
+
it("un-ignores each body file by name, and hands the paper to every file of it", () => {
|
|
282
|
+
const main = absolutePath("/papers/p/paper.tex");
|
|
283
|
+
const intro = absolutePath("/papers/p/sections/intro.tex");
|
|
284
|
+
expect(includeBlocks([{ dir: "/papers/p", main, files: [intro] }])).toEqual(
|
|
285
|
+
[
|
|
286
|
+
{ basePath: "/papers/p", ignores: ["!sections/intro.tex"] },
|
|
287
|
+
{
|
|
288
|
+
basePath: "/papers/p",
|
|
289
|
+
files: ["paper.tex", "sections/intro.tex"],
|
|
290
|
+
settings: { paperlint: { paper: { main, files: [main, intro] } } },
|
|
291
|
+
},
|
|
292
|
+
],
|
|
185
293
|
);
|
|
186
294
|
});
|
|
295
|
+
|
|
296
|
+
it("a paper of one file un-ignores nothing", () => {
|
|
297
|
+
const main = absolutePath("/papers/p/paper.tex");
|
|
298
|
+
expect(includeBlocks([{ dir: "/papers/p", main, files: [] }])).toEqual([
|
|
299
|
+
{
|
|
300
|
+
basePath: "/papers/p",
|
|
301
|
+
files: ["paper.tex"],
|
|
302
|
+
settings: { paperlint: { paper: { main, files: [main] } } },
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
187
306
|
});
|
package/src/paper-includes.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* THE FILES OF A PAPER'S BODY, handed to ESLint — so the rules over ESLint's LaTeX text read a paper
|
|
3
3
|
* whose body is in `sections/*.tex`, and report each finding at its own file and line (#144).
|
|
4
4
|
*
|
|
5
|
-
* The set is what TeX
|
|
6
|
-
* src/
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The set is what TeX read, as the last build recorded it (`_build/sources.json`, `paperRecord` in
|
|
6
|
+
* src/paper-record.ts): `paper.tex` and the `.tex` files of the paper directory TeX read after
|
|
7
|
+
* `\\begin{document}`. Nothing here reads TeX source or looks for an include on disk. Not a glob over
|
|
8
|
+
* every `.tex`: a frozen version under `versions/`, a macro file the preamble includes and a scratch
|
|
9
|
+
* file nothing includes are not the paper. A paper with no record, or one it has changed since, is
|
|
10
|
+
* `paper.tex` alone — `paper/sources-fresh` (src/sources-rules.ts) says the rest went unlinted.
|
|
9
11
|
*
|
|
10
12
|
* Two blocks per paper, both relative to the paper's directory (`basePath`):
|
|
11
13
|
*
|
|
@@ -16,21 +18,32 @@
|
|
|
16
18
|
* for the rules whose verdict is the paper's (eslint-rules/paper-context.ts).
|
|
17
19
|
*
|
|
18
20
|
* ── ONLY `.tex` FILES ARE HANDED TO ESLint (#153) ────────────────────────────────
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* TeX may read a body file of another extension: `\\input{paper.bbl}` in a paper that ships bibtex's
|
|
22
|
+
* output instead of a `.bib`, a plot exported as `.pgf`. Handed to ESLint, such a file got no
|
|
23
|
+
* language — the fragment block claims `.tex` only — and the JavaScript parser stopped at its first
|
|
24
|
+
* `%`. It is not given the LaTeX language either: the fragment rules judge and `--fix` the prose an
|
|
25
|
+
* author writes in that file, and a file of another extension is in practice a tool's output, whose
|
|
26
|
+
* words are someone else's (the titles of cited works) and whose fix the next run of the tool
|
|
27
|
+
* overwrites. TeX still reads it, and so does `readPaper`: the rules over the whole paper see it
|
|
28
|
+
* spliced into `paper.tex`, where TeX typesets it.
|
|
29
|
+
*
|
|
30
|
+
* ── NOR IS A `.tex` TeX WROTE ────────────────────────────────────────────────────
|
|
31
|
+
* A `filecontents` block may write a `.tex` the body `\\input`s (the record lists it as an input and
|
|
32
|
+
* as written, src/domain/tex-run.ts). Its words are the block's, which the author edits in the file
|
|
33
|
+
* holding the block; a fix made in the written copy is overwritten by an `[overwrite]` block on the next
|
|
34
|
+
* build, and splits the copy from the block without one. So it is read the way TeX reads it — spliced by
|
|
35
|
+
* `readPaper`, for the rules over the whole paper — and is not handed to ESLint as a file of its own.
|
|
27
36
|
*/
|
|
28
37
|
import { extname, join, relative } from "node:path";
|
|
29
38
|
import { PAPERLINT_SETTINGS } from "#eslint-rules/paper-context";
|
|
30
39
|
import { callerPath } from "./caller-path.ts";
|
|
31
40
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
41
|
+
import {
|
|
42
|
+
paperRecord,
|
|
43
|
+
recordedFiles,
|
|
44
|
+
type RecordReadDeps,
|
|
45
|
+
} from "./paper-record.ts";
|
|
32
46
|
import type { ConfigBlock } from "./rules-config.ts";
|
|
33
|
-
import { bodyFiles, type PaperDeps, type Unread } from "./tex-paper.ts";
|
|
34
47
|
|
|
35
48
|
/** The files the fragment block (`buildConfig`) claims: the only body files ESLint is handed. */
|
|
36
49
|
export const FRAGMENT_FILES: readonly string[] = ["**/*.tex"];
|
|
@@ -38,27 +51,31 @@ export const FRAGMENT_FILES: readonly string[] = ["**/*.tex"];
|
|
|
38
51
|
/** Whether a body file is one the fragment block claims — the extension its glob names. */
|
|
39
52
|
const isFragment = (file: string): boolean => extname(file) === ".tex";
|
|
40
53
|
|
|
41
|
-
/** One paper's main file and body files
|
|
54
|
+
/** One paper's main file and the body files of its last build. */
|
|
42
55
|
export interface PaperBody {
|
|
43
56
|
readonly dir: string;
|
|
44
57
|
readonly main: AbsolutePath;
|
|
45
|
-
/** The body files ESLint lints, each as a fragment: the `.tex` ones. */
|
|
58
|
+
/** The body files ESLint lints, each as a fragment: the `.tex` ones TeX did not write. None without a fresh record. */
|
|
46
59
|
readonly files: readonly AbsolutePath[];
|
|
47
|
-
readonly missing: readonly Unread[];
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** The body of each paper directory in `dirs` that has a `paper.tex`. */
|
|
51
63
|
export function paperBodies(
|
|
52
64
|
dirs: readonly string[],
|
|
53
|
-
deps:
|
|
65
|
+
deps: RecordReadDeps,
|
|
54
66
|
): readonly PaperBody[] {
|
|
55
67
|
return dirs.flatMap((dir) => {
|
|
56
68
|
const main = callerPath(join(dir, "paper.tex"));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
69
|
+
if (!deps.files.isFile(main)) return [];
|
|
70
|
+
const record = paperRecord(dir, deps);
|
|
71
|
+
if (record.kind !== "fresh") return [{ dir, main, files: [] }];
|
|
72
|
+
const written = new Set(
|
|
73
|
+
record.record.written.map((w) => callerPath(join(dir, w))),
|
|
74
|
+
);
|
|
75
|
+
const files = recordedFiles(dir, record.record, "body").filter(
|
|
76
|
+
(f) => f !== main && isFragment(f) && !written.has(f),
|
|
77
|
+
);
|
|
78
|
+
return [{ dir, main, files }];
|
|
62
79
|
});
|
|
63
80
|
}
|
|
64
81
|
|
|
@@ -80,16 +97,3 @@ export const includeBlocks = (
|
|
|
80
97
|
},
|
|
81
98
|
];
|
|
82
99
|
});
|
|
83
|
-
|
|
84
|
-
/** One line per include that resolved nowhere: which text lint did not read, and why. */
|
|
85
|
-
export const unreadLines = (
|
|
86
|
-
bodies: readonly PaperBody[],
|
|
87
|
-
shown: (path: string) => string,
|
|
88
|
-
): readonly string[] =>
|
|
89
|
-
bodies.flatMap((b) =>
|
|
90
|
-
b.missing.map(
|
|
91
|
-
(m) =>
|
|
92
|
-
`${shown(b.main)}: lint did not read \`${m.target}\` (included by ${m.file}): ` +
|
|
93
|
-
"no such file in the paper's directory or paperlint's inputs",
|
|
94
|
-
),
|
|
95
|
-
);
|
package/src/paper-prose.test.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { expect, it } from "vitest";
|
|
2
2
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
3
3
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
4
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
4
5
|
import { paperProse } from "./paper-prose.ts";
|
|
6
|
+
import { builtFixture, builtPaper } from "../test/recorded-fixture.ts";
|
|
5
7
|
|
|
6
8
|
const SRC = [
|
|
7
9
|
"\\documentclass{article}",
|
|
@@ -20,12 +22,23 @@ const SRC = [
|
|
|
20
22
|
].join("\n");
|
|
21
23
|
|
|
22
24
|
it("the body's prose, a blank line between passages, includes spliced; the preamble, comments, the inline .bib and the title stay out", () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// The build recorded TeX reading both files; the preamble holds no include.
|
|
26
|
+
const files = memoryFiles(
|
|
27
|
+
builtPaper(
|
|
28
|
+
"/p",
|
|
29
|
+
{ "paper.tex": SRC, "body.tex": "\\subsection{Method}\nends here." },
|
|
30
|
+
[
|
|
31
|
+
{ path: "paper.tex", role: "body" },
|
|
32
|
+
{ path: "body.tex", role: "body" },
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
);
|
|
27
36
|
expect(
|
|
28
|
-
paperProse("/p/paper.tex", SRC, {
|
|
37
|
+
paperProse("/p/paper.tex", SRC, {
|
|
38
|
+
files,
|
|
39
|
+
latex: latexReader,
|
|
40
|
+
codec: sourcesCodec,
|
|
41
|
+
}),
|
|
29
42
|
).toEqual({
|
|
30
43
|
// A citation stands as `[1]`, where it is set; a heading — here an included one — ends a passage.
|
|
31
44
|
body: "We measure it [1].\n\nA second paragraph\n\nends here.",
|
|
@@ -44,6 +57,22 @@ it("a citation stands as `[1]` and a cross-reference as `1`, so the sentence sti
|
|
|
44
57
|
paperProse("/p/paper.tex", src, {
|
|
45
58
|
files: memoryFiles({ "/p/paper.tex": src }),
|
|
46
59
|
latex: latexReader,
|
|
60
|
+
codec: sourcesCodec,
|
|
47
61
|
}).body,
|
|
48
62
|
).toBe("It holds [1]. It is shown (Section 1).");
|
|
49
63
|
});
|
|
64
|
+
|
|
65
|
+
it("🔴 a .tex a filecontents block wrote and the body \\input's is prose once: spliced where TeX read it, the block's body no prose", () => {
|
|
66
|
+
// v29-generated-input: TeX recorded body.tex — written by the block, read after \begin{document}.
|
|
67
|
+
const files = builtFixture("v29-generated-input", "/p");
|
|
68
|
+
const src = files["/p/paper.tex"] ?? "";
|
|
69
|
+
const { body } = paperProse("/p/paper.tex", src, {
|
|
70
|
+
files: memoryFiles(files),
|
|
71
|
+
latex: latexReader,
|
|
72
|
+
codec: sourcesCodec,
|
|
73
|
+
});
|
|
74
|
+
expect(
|
|
75
|
+
body.match(/This sentence is typeset from a file a block wrote\./g),
|
|
76
|
+
).toHaveLength(1);
|
|
77
|
+
expect(body).toMatch(/^Before the generated file\. This sentence/);
|
|
78
|
+
});
|
package/src/paper-prose.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { bodySentences } from "./domain/register.ts";
|
|
18
18
|
import { CITATION, MATH, REFERENCE } from "./domain/sentences.ts";
|
|
19
19
|
import { runText } from "./domain/tex-document.ts";
|
|
20
|
-
import { readPaper, type
|
|
20
|
+
import { readPaper, type ProseDeps } from "./tex-paper.ts";
|
|
21
21
|
|
|
22
22
|
/** A sentence's text with each mark as a reader would see it set, and math left out. */
|
|
23
23
|
const rendered = (raw: string): string =>
|
|
@@ -43,7 +43,7 @@ export interface PaperProse {
|
|
|
43
43
|
export function paperProse(
|
|
44
44
|
filename: string,
|
|
45
45
|
src: string,
|
|
46
|
-
deps:
|
|
46
|
+
deps: ProseDeps,
|
|
47
47
|
): PaperProse {
|
|
48
48
|
const { text, segments } = readPaper(filename, src, deps);
|
|
49
49
|
const passages = deps.latex
|