paperlint 7.2.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 +41 -30
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +9 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -68
- 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/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +6 -5
- 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 +34 -96
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.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/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.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-files.d.ts +4 -4
- 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 +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- 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/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- 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/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +18 -12
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +4 -6
- package/docs/package-shape-options.md +1 -1
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- 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/tex/claim-provenance.md +0 -1
- package/docs/rules.md +62 -61
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +6 -5
- package/eslint-rules/paper-typography.test.mjs +29 -157
- package/eslint-rules/paper-typography.ts +35 -104
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +22 -1
- 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/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +2 -12
- package/presets/realm.jsonc +1 -1
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +15 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +117 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- 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 -32
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- 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.harness.mjs +2 -2
- package/src/build.test.ts +211 -1
- package/src/build.ts +101 -41
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +46 -56
- package/src/cli.test.ts +44 -48
- package/src/cli.ts +51 -82
- 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/folder-venue-rule.test.ts +1 -1
- package/src/heading-case.test.ts +24 -10
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/lint-fix.test.ts +10 -4
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +12 -30
- package/src/new-paper.test.ts +15 -11
- package/src/new-paper.ts +11 -30
- package/src/package-dirs.ts +3 -2
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +78 -0
- package/src/paper-prose.ts +67 -0
- 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/parse-args.test.ts +0 -1
- 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/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- 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/src/types.ts +0 -2
- 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/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
checkNvd,
|
|
25
25
|
checkCommit,
|
|
26
26
|
reduceVerdict,
|
|
27
|
-
|
|
27
|
+
citationsOf,
|
|
28
28
|
parseArxivFeed,
|
|
29
29
|
normalizeDoi,
|
|
30
30
|
normalizeArxiv,
|
|
@@ -33,6 +33,12 @@ import {
|
|
|
33
33
|
titleIdentity,
|
|
34
34
|
isValidCveId,
|
|
35
35
|
} from "./verify-cites.mjs";
|
|
36
|
+
import { bibReader } from "#src/adapters/bibtex/index";
|
|
37
|
+
import { absolutePath } from "#src/domain/paths";
|
|
38
|
+
|
|
39
|
+
/** The citations of a BibTeX text, read by the one bibtex reader. */
|
|
40
|
+
const citationsIn = (bib) =>
|
|
41
|
+
citationsOf(bibReader.readFile(absolutePath("/p/refs.bib"), bib).entries);
|
|
36
42
|
|
|
37
43
|
/** One vitest case per check, named by it. */
|
|
38
44
|
function ok(cond, name) {
|
|
@@ -377,7 +383,7 @@ console.log("\n(extra) .bib parser best-effort extraction:");
|
|
|
377
383
|
eprint={1512.03385}
|
|
378
384
|
}
|
|
379
385
|
`;
|
|
380
|
-
const cites =
|
|
386
|
+
const cites = citationsIn(bib);
|
|
381
387
|
eq(cites.length, 2, "parsed 2 entries");
|
|
382
388
|
eq(cites[0].doi, "10.5555/3295222.3295349", "entry 1 doi");
|
|
383
389
|
eq(cites[1].arxiv, "1512.03385", "entry 2 arxiv eprint");
|
|
@@ -724,7 +730,7 @@ console.log("\n[S4] .bib parser edge cases:");
|
|
|
724
730
|
title = {The Real Paper Title},
|
|
725
731
|
year = {2021}
|
|
726
732
|
}`;
|
|
727
|
-
const a =
|
|
733
|
+
const a = citationsIn(bibA)[0];
|
|
728
734
|
eq(a.title, "The Real Paper Title", "title anchored — not booktitle");
|
|
729
735
|
|
|
730
736
|
// (b) brace-protected acronym must not truncate at the inner brace.
|
|
@@ -733,14 +739,14 @@ console.log("\n[S4] .bib parser edge cases:");
|
|
|
733
739
|
year = {2026}
|
|
734
740
|
}`;
|
|
735
741
|
eq(
|
|
736
|
-
|
|
742
|
+
citationsIn(bibB)[0].title,
|
|
737
743
|
"Evaluating LLM-based Systems for Code Review",
|
|
738
744
|
"brace-protected acronym kept whole",
|
|
739
745
|
);
|
|
740
746
|
|
|
741
747
|
// (c1) one-line entry (no \n} at column 0).
|
|
742
748
|
const bibC1 = `@article{k3, title = {A One Line Entry}, year = {2020}, doi = {10.1/abc}}`;
|
|
743
|
-
const c1 =
|
|
749
|
+
const c1 = citationsIn(bibC1)[0];
|
|
744
750
|
eq(c1.title, "A One Line Entry", "one-line entry title parsed");
|
|
745
751
|
eq(c1.doi, "10.1/abc", "one-line entry doi parsed");
|
|
746
752
|
|
|
@@ -750,14 +756,14 @@ console.log("\n[S4] .bib parser edge cases:");
|
|
|
750
756
|
year = {2019}
|
|
751
757
|
}`;
|
|
752
758
|
eq(
|
|
753
|
-
|
|
759
|
+
citationsIn(bibC2)[0].title,
|
|
754
760
|
"Indented Closing Brace",
|
|
755
761
|
"indented-closing-brace entry parsed",
|
|
756
762
|
);
|
|
757
763
|
|
|
758
764
|
// all four parse together, in sequence (balanced-brace scan resumes correctly).
|
|
759
765
|
eq(
|
|
760
|
-
|
|
766
|
+
citationsIn(bibA + "\n" + bibB + "\n" + bibC1 + "\n" + bibC2).length,
|
|
761
767
|
4,
|
|
762
768
|
"all 4 entries parsed in sequence",
|
|
763
769
|
);
|
|
@@ -796,15 +802,6 @@ console.log("\n[S4] .bib parser edge cases:");
|
|
|
796
802
|
});
|
|
797
803
|
eq(n.doi, "10.1/y", "normalizeIdentifiers cleans doi");
|
|
798
804
|
eq(n.arxiv, "1234.5678", "normalizeIdentifiers cleans arxiv");
|
|
799
|
-
// the exact live-reproduced break: a \bibitem DOI with a swallowed sentence period.
|
|
800
|
-
const bib = `\\begin{thebibliography}{9}
|
|
801
|
-
\\bibitem{ccs23} A. Author. Title. In Proc. CCS. doi:10.1145/3576915.3623218. 2023.
|
|
802
|
-
\\end{thebibliography}`;
|
|
803
|
-
eq(
|
|
804
|
-
parseBib(bib)[0].doi,
|
|
805
|
-
"10.1145/3576915.3623218",
|
|
806
|
-
"bibitem DOI trailing period not swallowed",
|
|
807
|
-
);
|
|
808
805
|
}
|
|
809
806
|
|
|
810
807
|
// ── Edges of the pure layer: the defensive arms, each with the input that reaches it ──────────
|
|
@@ -916,32 +913,22 @@ test("reduceVerdict: an arXiv id missed only by a non-authoritative source is un
|
|
|
916
913
|
});
|
|
917
914
|
});
|
|
918
915
|
|
|
919
|
-
test("
|
|
916
|
+
test("citationsOf: @comment/@string/@preamble are no citations; a quoted and a bare value are read; authors written out", () => {
|
|
920
917
|
const bib = [
|
|
921
918
|
"@comment{c, not an entry}",
|
|
922
|
-
"@string{s
|
|
923
|
-
'@article{k1, title = "Quoted Title", year = 2017, eprint = {1706.03762}, archiveprefix = {arXiv}}',
|
|
924
|
-
"@misc{k2, title = {T},
|
|
925
|
-
].join("\n");
|
|
926
|
-
assert.deepEqual(parseBib(bib), [
|
|
927
|
-
{ id: "k1", title: "Quoted Title", year: "2017", arxiv: "1706.03762" },
|
|
928
|
-
{ id: "k2", title: "T" },
|
|
929
|
-
]);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
test("parseBib's \\bibitem fallback reads an arXiv id and an \\emph title", () => {
|
|
933
|
-
const text = [
|
|
934
|
-
"\\begin{thebibliography}{9}",
|
|
935
|
-
"\\bibitem{a} A. Author. \\emph{A Long Enough Title}. arXiv:1706.03762, 2017.",
|
|
936
|
-
"\\end{thebibliography}",
|
|
919
|
+
"@string{s = {y}}",
|
|
920
|
+
'@article{k1, title = "Quoted Title", year = 2017, eprint = {1706.03762}, archiveprefix = {arXiv}, author = {Vaswani, Ashish and von Neumann, John}}',
|
|
921
|
+
"@misc{k2, title = {T}, eprint = {x}, archiveprefix = {HAL}}",
|
|
937
922
|
].join("\n");
|
|
938
|
-
assert.deepEqual(
|
|
923
|
+
assert.deepEqual(citationsIn(bib), [
|
|
939
924
|
{
|
|
940
|
-
id: "
|
|
941
|
-
|
|
925
|
+
id: "k1",
|
|
926
|
+
title: "Quoted Title",
|
|
942
927
|
year: "2017",
|
|
943
|
-
|
|
928
|
+
authors: "Ashish Vaswani and John von Neumann",
|
|
929
|
+
arxiv: "1706.03762",
|
|
944
930
|
},
|
|
931
|
+
{ id: "k2", title: "T" },
|
|
945
932
|
]);
|
|
946
933
|
});
|
|
947
934
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bibtex reader over @retorquere/bibtex-parser: where each entry stands, its fields as text, its
|
|
3
|
+
* name lists as names, the `@string` and `@preamble` a copy is compared on, and an entry it cannot
|
|
4
|
+
* finish. What bibtex reads beyond it is the post-build check's (src/references.ts), measured on the
|
|
5
|
+
* planted papers in src/paper-sources.test.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { absolutePath } from "../../domain/paths.ts";
|
|
12
|
+
import { bibReader } from "./index.ts";
|
|
13
|
+
|
|
14
|
+
const PATH = absolutePath("/p/refs.bib");
|
|
15
|
+
const read = (text: string) => bibReader.readFile(PATH, text);
|
|
16
|
+
|
|
17
|
+
describe("the bibtex reader", () => {
|
|
18
|
+
it("places each entry in the text, a block's within the file that holds it", () => {
|
|
19
|
+
const tex =
|
|
20
|
+
"\\begin{filecontents*}{refs.bib}\n@misc{a, title={A}}\n@Article(b, title={B})\n\\end{filecontents*}\n";
|
|
21
|
+
const body = { start: tex.indexOf("@misc"), end: tex.indexOf("\\end") };
|
|
22
|
+
const bib = bibReader.read(absolutePath("/p/paper.tex"), tex, body);
|
|
23
|
+
expect(
|
|
24
|
+
bib.entries.map((e) => [
|
|
25
|
+
e.type,
|
|
26
|
+
e.key,
|
|
27
|
+
tex.slice(e.span.start, e.span.end),
|
|
28
|
+
]),
|
|
29
|
+
).toEqual([
|
|
30
|
+
["misc", "a", "@misc{a, title={A}}"],
|
|
31
|
+
["article", "b", "@Article(b, title={B})"],
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reads fields as text: LaTeX accents, a link, a list field joined", () => {
|
|
36
|
+
const [e] = read(
|
|
37
|
+
'@book{k, title={M{\\"u}ller}, howpublished={\\url{https://x.org}}, publisher={A and B}}',
|
|
38
|
+
).entries;
|
|
39
|
+
// The parser writes `{\"u}` as u and a combining diaeresis: compared composed.
|
|
40
|
+
expect(
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
Object.entries(e?.fields ?? {}).map(([k, v]) => [
|
|
43
|
+
k,
|
|
44
|
+
v.normalize("NFC"),
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
).toEqual({
|
|
48
|
+
title: "Müller",
|
|
49
|
+
howpublished: '<a href="https://x.org">https://x.org</a>',
|
|
50
|
+
publisher: "A and B",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reads a name list as names: family part, particle, suffix, a literal kept whole", () => {
|
|
55
|
+
const [e] = read(
|
|
56
|
+
"@misc{k, author={Lovelace, Ada and von Neumann, Jr., John and {World Health Organization} and others}}",
|
|
57
|
+
).entries;
|
|
58
|
+
expect(e?.names).toEqual({
|
|
59
|
+
author: [
|
|
60
|
+
{ lastName: "Lovelace", firstName: "Ada" },
|
|
61
|
+
{
|
|
62
|
+
lastName: "Neumann",
|
|
63
|
+
firstName: "John",
|
|
64
|
+
prefix: "von",
|
|
65
|
+
suffix: "Jr.",
|
|
66
|
+
},
|
|
67
|
+
{ name: "World Health Organization" },
|
|
68
|
+
{ lastName: "others" },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("the bibtex reader — what a copy is compared on, and what it leaves to bibtex", () => {
|
|
75
|
+
it("reads @preamble commands, and expands each @string where an entry uses it", () => {
|
|
76
|
+
const bib = read(
|
|
77
|
+
'@string{v = "Venue"}\n@preamble{"\\x"}\n@misc{k, note = v}\n@string{v = "Later"}',
|
|
78
|
+
);
|
|
79
|
+
expect([bib.preamble, bib.entries[0]?.fields]).toEqual([
|
|
80
|
+
['"\\x"'],
|
|
81
|
+
{ note: "Venue" },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps each entry as bibtex takes it: the value it used, LaTeX and braces as written, whitespace folded", () => {
|
|
86
|
+
const bib = read(
|
|
87
|
+
'@string{v = "Venue"}\n@Misc{k, note = v # " 2", author = {G{\\"o}del, Kurt}, title = {{X}}, keywords = {b, a}}\n@string{v = "Later"}',
|
|
88
|
+
);
|
|
89
|
+
expect(bib.written).toEqual([
|
|
90
|
+
{
|
|
91
|
+
type: "misc",
|
|
92
|
+
key: "k",
|
|
93
|
+
fields: {
|
|
94
|
+
note: "Venue 2",
|
|
95
|
+
author: 'G{\\"o}del, Kurt',
|
|
96
|
+
title: "{X}",
|
|
97
|
+
keywords: "a, b",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("an entry it cannot finish is no entry — bibtex reports that file (v6, exit 2)", () => {
|
|
104
|
+
const text = readFileSync(
|
|
105
|
+
join(
|
|
106
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
107
|
+
"../../../fixtures/paper-sources/v6-unclosed/refs.bib",
|
|
108
|
+
),
|
|
109
|
+
"utf8",
|
|
110
|
+
);
|
|
111
|
+
expect(read(text).entries.map((e) => e.key)).toEqual(["a1", "a3"]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("the bibtex reader — databases in sequence, as bibtex reads them", () => {
|
|
116
|
+
// bibtex reads the databases of `\\bibliography{abbreviations,references}` one after the other, and an
|
|
117
|
+
// `@string` the first defines is in force in the second (fixtures/paper-sources/v27-shared-string:
|
|
118
|
+
// its .bbl typesets the expanded author and booktitle).
|
|
119
|
+
const ABBR =
|
|
120
|
+
'@string{venue = "Proceedings of the {\\"O}ld Venue"}\n@string{who = "Lovelace, Ada"}\n';
|
|
121
|
+
const REFS =
|
|
122
|
+
"@inproceedings{late, author = who, booktitle = venue # {, Main}}\n";
|
|
123
|
+
const SECOND = absolutePath("/p/references.bib");
|
|
124
|
+
|
|
125
|
+
it("🔴 an @string an earlier database defined is in force in a later one: read as one file holding both", () => {
|
|
126
|
+
const first = read(ABBR);
|
|
127
|
+
const second = bibReader.readFile(SECOND, REFS, first.strings);
|
|
128
|
+
const whole = read(ABBR + REFS);
|
|
129
|
+
expect([
|
|
130
|
+
second.entries.map((e) => [e.fields, e.names]),
|
|
131
|
+
second.written,
|
|
132
|
+
]).toEqual([whole.entries.map((e) => [e.fields, e.names]), whole.written]);
|
|
133
|
+
expect(second.written[0]?.fields).toEqual({
|
|
134
|
+
author: "Lovelace, Ada",
|
|
135
|
+
booktitle: 'Proceedings of the {\\"O}ld Venue, Main',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("the @strings in force after a database are those before it and its own, a redefinition winning; a block reads with them too", () => {
|
|
140
|
+
const first = read(ABBR);
|
|
141
|
+
const second = bibReader.readFile(
|
|
142
|
+
SECOND,
|
|
143
|
+
'@string{venue = "Newer"}\n',
|
|
144
|
+
first.strings,
|
|
145
|
+
);
|
|
146
|
+
expect([second.inherited, second.strings]).toEqual([
|
|
147
|
+
first.strings,
|
|
148
|
+
{ VENUE: "Newer", WHO: "Lovelace, Ada" },
|
|
149
|
+
]);
|
|
150
|
+
const tex = `\\begin{filecontents*}{refs.bib}\n${REFS}\\end{filecontents*}\n`;
|
|
151
|
+
const body = { start: tex.indexOf("@"), end: tex.indexOf("\\end") };
|
|
152
|
+
expect(
|
|
153
|
+
bibReader.read(absolutePath("/p/paper.tex"), tex, body, second.strings)
|
|
154
|
+
.written[0]?.fields.booktitle,
|
|
155
|
+
).toBe("Newer, Main");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("read alone, a database has nothing inherited: a name no @string defines stays unresolved", () => {
|
|
159
|
+
const alone = bibReader.readFile(SECOND, REFS);
|
|
160
|
+
expect([alone.inherited, alone.strings]).toEqual([{}, {}]);
|
|
161
|
+
expect(alone.written[0]?.fields.author).not.toBe("Lovelace, Ada");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bibtex reader over `@retorquere/bibtex-parser` — the one `.bib` reader of paperlint
|
|
3
|
+
* (`src/ports/bib-reader.ts`).
|
|
4
|
+
*
|
|
5
|
+
* It reads a well-formed database as bibtex does: entries, fields (LaTeX read into text, so `{\"u}`
|
|
6
|
+
* is `ü` and `\url{x}` a link), name lists split into names, `@string`s expanded, `@preamble`. A
|
|
7
|
+
* second reading keeps each field as bibtex takes it, LaTeX as written: what two databases are
|
|
8
|
+
* compared by (`sameDatabase`), where `{\"u}` and `ü` must stay different. A malformed
|
|
9
|
+
* database is bibtex's to report: the build runs bibtex and fails with bibtex's own lines. The two
|
|
10
|
+
* readers also differ on one well-formed shape: bibtex has no comment syntax, so an entry behind
|
|
11
|
+
* `%` or inside `@comment{…}` is one bibtex reads and this reader does not. The build records what
|
|
12
|
+
* bibtex read, and `paper/refs-checked` names such an entry (`src/references.ts`).
|
|
13
|
+
*
|
|
14
|
+
* The parser gives each entry's text (`input`), not its place: each is found in the body in order.
|
|
15
|
+
*
|
|
16
|
+
* A database read after others starts with the `@string`s they left in force (`inherited`), as bibtex
|
|
17
|
+
* reads a paper's databases in sequence with one table of them: the parser takes them as its table of
|
|
18
|
+
* predefined `@string`s, which the database's own definitions override from where they stand. Each
|
|
19
|
+
* database is still parsed as its own text, so every entry keeps its place in the file that holds it.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
parse,
|
|
23
|
+
type Creator,
|
|
24
|
+
type Entry,
|
|
25
|
+
type Library,
|
|
26
|
+
type Options,
|
|
27
|
+
} from "@retorquere/bibtex-parser";
|
|
28
|
+
import type {
|
|
29
|
+
BibEntry,
|
|
30
|
+
BibName,
|
|
31
|
+
BibStrings,
|
|
32
|
+
BibText,
|
|
33
|
+
WrittenEntry,
|
|
34
|
+
} from "../../domain/paper-sources.ts";
|
|
35
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
36
|
+
import type { Span } from "../../domain/tex-document.ts";
|
|
37
|
+
import type { BibReader } from "../../ports/bib-reader.ts";
|
|
38
|
+
|
|
39
|
+
/** A title as written, not sentence-cased; no field kept as raw LaTeX. */
|
|
40
|
+
const OPTIONS: Readonly<Options> = { sentenceCase: false, verbatimFields: [] };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every field as bibtex takes it: `@string`s expanded where they are used, LaTeX left as written (no
|
|
44
|
+
* conversion, no name split, no outer braces dropped).
|
|
45
|
+
*/
|
|
46
|
+
const AS_WRITTEN: Readonly<Options> = {
|
|
47
|
+
raw: true,
|
|
48
|
+
sentenceCase: false,
|
|
49
|
+
verbatimFields: [/.*/],
|
|
50
|
+
removeOuterBraces: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** A name's parts, those it has: the parser leaves the others out. */
|
|
54
|
+
const nameOf = (c: Readonly<Creator>): BibName =>
|
|
55
|
+
Object.fromEntries(
|
|
56
|
+
Object.entries({
|
|
57
|
+
lastName: c.lastName,
|
|
58
|
+
firstName: c.firstName,
|
|
59
|
+
prefix: c.prefix,
|
|
60
|
+
suffix: c.suffix,
|
|
61
|
+
name: c.name,
|
|
62
|
+
}).filter((kv): kv is [string, string] => kv[1] !== undefined),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const isText = (x: unknown): x is string => typeof x === "string";
|
|
66
|
+
const isName = (x: unknown): x is Readonly<Creator> =>
|
|
67
|
+
typeof x === "object" && x !== null;
|
|
68
|
+
|
|
69
|
+
/** An entry's fields and name lists, from the parser's one record of both. */
|
|
70
|
+
function fieldsOf(e: Readonly<Entry>): Pick<BibEntry, "fields" | "names"> {
|
|
71
|
+
const pairs = Object.entries(e.fields);
|
|
72
|
+
const isNames = (k: string): boolean => e.mode[k] === "creatorlist";
|
|
73
|
+
return {
|
|
74
|
+
// A list field (`keywords`, `publisher`) is joined as BibTeX writes it.
|
|
75
|
+
fields: Object.fromEntries(
|
|
76
|
+
pairs
|
|
77
|
+
.filter(([k]) => !isNames(k))
|
|
78
|
+
.map(([k, v]) => [k, [v].flat().filter(isText).join(" and ")]),
|
|
79
|
+
),
|
|
80
|
+
names: Object.fromEntries(
|
|
81
|
+
pairs
|
|
82
|
+
.filter(([k]) => isNames(k))
|
|
83
|
+
.map(([k, v]) => [k, [v].flat().filter(isName).map(nameOf)]),
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Each entry the parser read, placed in the text: found in `source` in order, `source` at `offset`. */
|
|
89
|
+
function placed(
|
|
90
|
+
parsed: readonly Readonly<Entry>[],
|
|
91
|
+
source: string,
|
|
92
|
+
offset: number,
|
|
93
|
+
): readonly BibEntry[] {
|
|
94
|
+
return parsed.reduce<{
|
|
95
|
+
readonly from: number;
|
|
96
|
+
readonly found: readonly BibEntry[];
|
|
97
|
+
}>(
|
|
98
|
+
(acc, e) => {
|
|
99
|
+
const at = source.indexOf(e.input, acc.from);
|
|
100
|
+
const end = at + e.input.length;
|
|
101
|
+
const span = { start: offset + at, end: offset + end };
|
|
102
|
+
const entry = { type: e.type.toLowerCase(), key: e.key, span };
|
|
103
|
+
return { from: end, found: [...acc.found, { ...entry, ...fieldsOf(e) }] };
|
|
104
|
+
},
|
|
105
|
+
{ from: 0, found: [] },
|
|
106
|
+
).found;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The entries the parser finished: one it could not keeps no text — it is malformed, and bibtex reports it. */
|
|
110
|
+
const finished = (lib: Readonly<Library>): readonly Readonly<Entry>[] =>
|
|
111
|
+
lib.entries.filter((e) => e.input !== "");
|
|
112
|
+
|
|
113
|
+
/** An entry as bibtex takes it; a list field (`keywords`) as one value. */
|
|
114
|
+
const writtenOf = (e: Readonly<Entry>): WrittenEntry => ({
|
|
115
|
+
type: e.type.toLowerCase(),
|
|
116
|
+
key: e.key,
|
|
117
|
+
fields: Object.fromEntries(
|
|
118
|
+
Object.entries(e.fields).map(([k, v]) => [
|
|
119
|
+
k,
|
|
120
|
+
[v].flat().filter(isText).join(", "),
|
|
121
|
+
]),
|
|
122
|
+
),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The database in `body` of `text`, the text of the file at `path`, read with the `@string`s
|
|
127
|
+
* `inherited` in force. The parser is handed a copy of them: it writes into its own.
|
|
128
|
+
*/
|
|
129
|
+
function read(
|
|
130
|
+
path: AbsolutePath,
|
|
131
|
+
text: string,
|
|
132
|
+
body: Span,
|
|
133
|
+
inherited: BibStrings = {},
|
|
134
|
+
): BibText {
|
|
135
|
+
const source = text.slice(body.start, body.end);
|
|
136
|
+
const lib = parse(source, { ...OPTIONS, strings: { ...inherited } });
|
|
137
|
+
const asWritten = parse(source, { ...AS_WRITTEN, strings: { ...inherited } });
|
|
138
|
+
return {
|
|
139
|
+
path,
|
|
140
|
+
text,
|
|
141
|
+
body,
|
|
142
|
+
entries: placed(finished(lib), source, body.start),
|
|
143
|
+
written: finished(asWritten).map(writtenOf),
|
|
144
|
+
preamble: lib.preamble,
|
|
145
|
+
inherited,
|
|
146
|
+
// Its own as bibtex takes them: the reading that keeps LaTeX as written.
|
|
147
|
+
strings: { ...inherited, ...asWritten.strings },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const bibReader: BibReader = {
|
|
152
|
+
read,
|
|
153
|
+
readFile: (path, text, inherited) =>
|
|
154
|
+
read(path, text, { start: 0, end: text.length }, inherited),
|
|
155
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE `filecontents` BLOCKS OF A SOURCE — where a `.bib` TeX wrote stands in the text that wrote it.
|
|
3
|
+
* Read from the parse tree, never decided: which file a block writes, and whether TeX ran it, is
|
|
4
|
+
* TeX's answer (`written` in `_build/sources.json`); this reader only says where each block's text
|
|
5
|
+
* is, so a finding about an entry TeX read can be shown at the block that holds it
|
|
6
|
+
* (docs/design/paper-sources.md §1).
|
|
7
|
+
*
|
|
8
|
+
* unified-latex reads a `filecontents` environment as verbatim; its option and file name are the
|
|
9
|
+
* first things in that node, white space between them — a line break too: `\begin{filecontents*}` with
|
|
10
|
+
* `{refs.bib}` on the next line writes `refs.bib` (measured with pdflatex) — and its body starts on the
|
|
11
|
+
* line after the name, as LaTeX's own reader starts it.
|
|
12
|
+
* A block is live when the tree keeps it: a comment is a node of its own, `\iffalse … \fi` is cut at
|
|
13
|
+
* parse (`conditionals.ts`), and nothing after `\end{document}` is read (`liveRoot`).
|
|
14
|
+
*/
|
|
15
|
+
import type * as Ast from "@unified-latex/unified-latex-types";
|
|
16
|
+
import type { Filecontents } from "../../ports/latex.ts";
|
|
17
|
+
import {
|
|
18
|
+
argumentPieces,
|
|
19
|
+
inPlace,
|
|
20
|
+
isNode,
|
|
21
|
+
placeOf,
|
|
22
|
+
visited,
|
|
23
|
+
type ArgumentPiece,
|
|
24
|
+
type Node,
|
|
25
|
+
} from "./nodes.ts";
|
|
26
|
+
import { liveRoot, parseLatex, type ParsedTex } from "./parse.ts";
|
|
27
|
+
|
|
28
|
+
const FILECONTENTS: ReadonlySet<string> = new Set([
|
|
29
|
+
"filecontents",
|
|
30
|
+
"filecontents*",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** Names and options as TeX reads them: off the tree, a comma between two names. */
|
|
34
|
+
type Name = readonly ArgumentPiece[];
|
|
35
|
+
|
|
36
|
+
/** What a name or an option is written as: its pieces joined, white space trimmed off. */
|
|
37
|
+
export const written = (name: Name): string =>
|
|
38
|
+
name
|
|
39
|
+
.map((p) => p.text)
|
|
40
|
+
.join("")
|
|
41
|
+
.trim();
|
|
42
|
+
|
|
43
|
+
/** What may stand before the name: nothing, or one `[options]` — anything else is a block with no name. */
|
|
44
|
+
const OPTION = /^(?:\[[^\]]*\])?$/;
|
|
45
|
+
|
|
46
|
+
/** A block's head: its name's group, and its options. */
|
|
47
|
+
interface Head {
|
|
48
|
+
readonly name: Node;
|
|
49
|
+
readonly options: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The head of a block whose text is `text`: the first group, with only white space and an option before it. */
|
|
53
|
+
function headOf(text: string): Head | null {
|
|
54
|
+
const nodes = parseLatex(text).root.content;
|
|
55
|
+
const at = nodes.findIndex((x) => x.type === "group");
|
|
56
|
+
const option = written(argumentPieces(nodes.slice(0, Math.max(at, 0)), null));
|
|
57
|
+
const name = nodes[at];
|
|
58
|
+
if (name === undefined || !OPTION.test(option)) return null;
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
options: option
|
|
62
|
+
.replace(/^\[|\]$/g, "")
|
|
63
|
+
.split(",")
|
|
64
|
+
.map((o) => o.trim()),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One verbatim `filecontents` node as a block: its name, option, and body. unified-latex reads the
|
|
70
|
+
* whole environment as verbatim, so its text is parsed on its own and the name read off that tree like
|
|
71
|
+
* a declaration's (`headOf`). The body starts on the line after the one the name ends on; what TeX
|
|
72
|
+
* ignores after the name on that line is no body.
|
|
73
|
+
*/
|
|
74
|
+
export function blockOf(
|
|
75
|
+
src: string,
|
|
76
|
+
n: Readonly<Ast.VerbatimEnvironment>,
|
|
77
|
+
jobname: string,
|
|
78
|
+
): readonly Filecontents[] {
|
|
79
|
+
return inPlace(placeOf(n), (span) => {
|
|
80
|
+
// The node runs from `\begin{env}` to past `\end{env}`.
|
|
81
|
+
const headAt = span.start + `\\begin{${n.env}}`.length;
|
|
82
|
+
const end = span.end - `\\end{${n.env}}`.length;
|
|
83
|
+
const head = headOf(src.slice(headAt, end));
|
|
84
|
+
if (head === null) return [];
|
|
85
|
+
return inPlace(placeOf(head.name), (named) => {
|
|
86
|
+
const after = headAt + named.end;
|
|
87
|
+
// What TeX ignores after the name, up to the end of its line.
|
|
88
|
+
const rest = src.slice(after, end).split("\n", 1).join("");
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
writes: written(argumentPieces([head.name], jobname)),
|
|
92
|
+
overwrite: head.options.some(
|
|
93
|
+
(o) => o === "overwrite" || o === "force",
|
|
94
|
+
),
|
|
95
|
+
span,
|
|
96
|
+
body: { start: Math.min(after + rest.length + 1, end), end },
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isBlock = (n: Node): n is Readonly<Ast.VerbatimEnvironment> =>
|
|
104
|
+
n.type === "verbatim" && FILECONTENTS.has(n.env);
|
|
105
|
+
|
|
106
|
+
/** Every live `filecontents` block of a source whose main file's name is `jobname`, in source order. */
|
|
107
|
+
export const filecontentsOf = (
|
|
108
|
+
t: ParsedTex,
|
|
109
|
+
jobname: string,
|
|
110
|
+
): readonly Filecontents[] =>
|
|
111
|
+
visited(liveRoot(t), () => false)
|
|
112
|
+
.filter(isNode)
|
|
113
|
+
.filter(isBlock)
|
|
114
|
+
.flatMap((n) => blockOf(t.src, n, jobname));
|
|
@@ -4,6 +4,7 @@ import type { Include } from "../../domain/paper-source.ts";
|
|
|
4
4
|
import type { Span } from "../../domain/tex-document.ts";
|
|
5
5
|
import { isDefinition } from "./hidden.ts";
|
|
6
6
|
import {
|
|
7
|
+
argumentPieces,
|
|
7
8
|
inPlace,
|
|
8
9
|
isNode,
|
|
9
10
|
macroPlace,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
type Macro,
|
|
14
15
|
type Node,
|
|
15
16
|
} from "./nodes.ts";
|
|
16
|
-
import type
|
|
17
|
+
import { liveRoot, type ParsedTex } from "./parse.ts";
|
|
17
18
|
|
|
18
19
|
const INCLUDE_MACROS: ReadonlyMap<string, Include["macro"]> = new Map([
|
|
19
20
|
["input", "input"],
|
|
@@ -22,15 +23,22 @@ const INCLUDE_MACROS: ReadonlyMap<string, Include["macro"]> = new Map([
|
|
|
22
23
|
]);
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* The path an include names
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* The path an include names: its braced argument read off the tree as TeX reads it (a comment is no
|
|
27
|
+
* part of it; a macro stays as written: nothing here expands one), or — TeX's own form, `\\input name`
|
|
28
|
+
* — the name up to the next space. unified-latex gives the unbraced form an argument of the first
|
|
29
|
+
* token only (`sections` of `sections/a`), so the source is read for that one.
|
|
28
30
|
*/
|
|
29
31
|
function argumentText(src: string, m: Macro): string | null {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
const arg = mandatory(m)[0];
|
|
33
|
+
const start = arg?.content[0]?.position?.start.offset;
|
|
34
|
+
if (arg === undefined || start === undefined) return null;
|
|
35
|
+
// The parser gives both forms `{` as their open mark; the source says which one was written.
|
|
36
|
+
if (src[start - 1] === "{")
|
|
37
|
+
return argumentPieces(arg.content, null)
|
|
38
|
+
.map((p) => p.text)
|
|
39
|
+
.join("");
|
|
32
40
|
const rest = src.slice(start);
|
|
33
|
-
const stop = rest.search(
|
|
41
|
+
const stop = rest.search(/[\s{}%]/u);
|
|
34
42
|
return stop < 0 ? rest : rest.slice(0, stop);
|
|
35
43
|
}
|
|
36
44
|
|
|
@@ -44,11 +52,11 @@ function includeOf(src: string, m: Macro): readonly Include[] {
|
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
54
|
* Every `\input`, `\include` and `\subfile` of the source, in source order — not in a comment (a
|
|
47
|
-
* comment is a node of its own)
|
|
48
|
-
* is used, and nothing here expands macros.
|
|
55
|
+
* comment is a node of its own), not in a macro definition's body, which is read where the macro
|
|
56
|
+
* is used, and nothing here expands macros; not after `\end{document}`, which TeX never reads.
|
|
49
57
|
*/
|
|
50
58
|
export const includesOf = (t: ParsedTex): readonly Include[] =>
|
|
51
|
-
visited(t
|
|
59
|
+
visited(liveRoot(t), isDefinition)
|
|
52
60
|
.filter(isNode)
|
|
53
61
|
.flatMap((n) => (n.type === "macro" ? includeOf(t.src, n) : []));
|
|
54
62
|
|