paperlint 3.1.3 → 3.2.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/.github/workflows/ci.yml +12 -7
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +34 -5
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +9 -5
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +2 -1
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +0 -5
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js +4 -3
- package/dist/adapters/node/process.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +13 -7
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +4 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts.map +1 -1
- package/dist/adapters/references/pool.js +4 -5
- package/dist/adapters/references/pool.js.map +1 -1
- package/dist/adapters/references/reach.io.d.ts.map +1 -1
- package/dist/adapters/references/reach.io.js +2 -1
- package/dist/adapters/references/reach.io.js.map +1 -1
- package/dist/build-engine.js +2 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +14 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +31 -34
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +3 -2
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts.map +1 -1
- package/dist/domain/lookup-cache.js +10 -8
- package/dist/domain/lookup-cache.js.map +1 -1
- package/dist/domain/text.d.ts +12 -0
- package/dist/domain/text.d.ts.map +1 -1
- package/dist/domain/text.js +17 -0
- package/dist/domain/text.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -3
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
- package/dist/eslint-rules/bib-reachable-entry.js +75 -0
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +80 -0
- package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
- package/dist/eslint-rules/install-path-literals.js +105 -0
- package/dist/eslint-rules/install-path-literals.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts +136 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -0
- package/dist/eslint-rules/latex-language.js +639 -0
- package/dist/eslint-rules/latex-language.js.map +1 -0
- package/dist/eslint-rules/paper-research-question.d.ts +31 -0
- package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
- package/dist/eslint-rules/paper-research-question.js +188 -0
- package/dist/eslint-rules/paper-research-question.js.map +1 -0
- package/dist/eslint-rules/paper-stages.d.ts +83 -0
- package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
- package/dist/eslint-rules/paper-stages.js +358 -0
- package/dist/eslint-rules/paper-stages.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +74 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
- package/dist/eslint-rules/paper-typography.js +404 -0
- package/dist/eslint-rules/paper-typography.js.map +1 -0
- package/dist/eslint-rules/papers.d.ts +42 -0
- package/dist/eslint-rules/papers.d.ts.map +1 -0
- package/dist/eslint-rules/papers.js +145 -0
- package/dist/eslint-rules/papers.js.map +1 -0
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
- package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
- package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
- package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
- package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
- package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
- package/dist/eslint-rules/review-frontmatter.js +126 -0
- package/dist/eslint-rules/review-frontmatter.js.map +1 -0
- package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
- package/dist/eslint-rules/rule-context.d.ts +55 -0
- package/dist/eslint-rules/rule-context.d.ts.map +1 -0
- package/dist/eslint-rules/rule-context.js +9 -0
- package/dist/eslint-rules/rule-context.js.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.js +16 -0
- package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
- package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
- package/dist/eslint-rules/temp-root-realpath.js +139 -0
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
- package/dist/eslint-rules/tex-build.d.ts +35 -0
- package/dist/eslint-rules/tex-build.d.ts.map +1 -0
- package/dist/eslint-rules/tex-build.js +313 -0
- package/dist/eslint-rules/tex-build.js.map +1 -0
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -1
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +14 -9
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +5 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +15 -7
- package/dist/init.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -2
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js +2 -2
- package/dist/latex-loop.js.map +1 -1
- package/dist/lib/agent-cli-version.d.ts +65 -0
- package/dist/lib/agent-cli-version.d.ts.map +1 -0
- package/dist/lib/agent-cli-version.js +77 -0
- package/dist/lib/agent-cli-version.js.map +1 -0
- package/dist/lib/check.d.ts +19 -0
- package/dist/lib/check.d.ts.map +1 -0
- package/dist/lib/check.js +52 -0
- package/dist/lib/check.js.map +1 -0
- package/dist/lib/markdown.d.ts +180 -0
- package/dist/lib/markdown.d.ts.map +1 -0
- package/dist/lib/markdown.js +302 -0
- package/dist/lib/markdown.js.map +1 -0
- package/dist/lib/paper-config.d.ts +84 -0
- package/dist/lib/paper-config.d.ts.map +1 -0
- package/dist/lib/paper-config.js +142 -0
- package/dist/lib/paper-config.js.map +1 -0
- package/dist/lib/skill-eval-fixture.d.ts +29 -0
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
- package/dist/lib/skill-eval-fixture.js +62 -0
- package/dist/lib/skill-eval-fixture.js.map +1 -0
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +5 -4
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +2 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +5 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +2 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -2
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +11 -6
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +6 -3
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +8 -3
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +21 -16
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.js +1 -1
- package/dist/venue-rules.js.map +1 -1
- package/docs/e2e.md +7 -7
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +2 -2
- package/docs/testing.md +11 -8
- package/docs/toolchain.md +1 -1
- package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +4 -3
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
- package/eslint-rules/latex-language.harness.mjs +2 -2
- package/eslint-rules/latex-language.test.mjs +7 -1
- package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
- package/eslint-rules/paper-research-question.harness.mjs +3 -3
- package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +34 -15
- package/eslint-rules/paper-stages.harness.mjs +2 -2
- package/eslint-rules/paper-stages.test.mjs +8 -2
- package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +74 -29
- package/eslint-rules/paper-typography.test.mjs +3 -3
- package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
- package/eslint-rules/papers.harness.mjs +2 -2
- package/eslint-rules/{papers.mjs → papers.ts} +7 -4
- package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
- package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +88 -31
- package/eslint-rules/review-frontmatter.test.mjs +2 -2
- package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
- package/eslint-rules/rule-context.ts +62 -0
- package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
- package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +19 -6
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
- package/eslint.config.mjs +139 -30
- package/fixtures/pdf-facts/README.md +2 -2
- package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
- package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
- package/hooks/paper-edit-guard.hook.mjs +1 -1
- package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
- package/hooks/paper-skills-nudge.hook.mjs +1 -1
- package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
- package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
- package/lib/agent-cli-version.harness.mjs +1 -1
- package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
- package/lib/check.test.ts +14 -9
- package/lib/{check.mjs → check.ts} +22 -9
- package/lib/markdown.test.mjs +7 -7
- package/lib/{markdown.mjs → markdown.ts} +68 -29
- package/lib/paper-config.harness.mjs +8 -8
- package/lib/{paper-config.mjs → paper-config.ts} +19 -14
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-checks.test.mjs +1 -1
- package/lib/skill-corpus.mjs +1 -1
- package/lib/skill-eval-kit.mjs +1 -1
- package/package.json +42 -10
- package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
- package/scripts/{check.test.mjs → check.test.ts} +13 -10
- package/scripts/{check.mjs → check.ts} +116 -42
- package/scripts/coverage-config.test.ts +80 -37
- package/scripts/eslint-report-guard.test.mjs +1 -1
- package/scripts/harness-api.frozen.json +14 -15
- package/scripts/harness-api.test.ts +8 -5
- package/scripts/layer-legacy-frozen.test.ts +17 -13
- package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
- package/scripts/release-config.test.ts +12 -4
- package/scripts/rules-see-files.test.mjs +1 -1
- package/scripts/test.test.ts +78 -0
- package/scripts/test.ts +87 -0
- package/skills/grade-paper-writing/prose-lint.mjs +1 -1
- package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
- package/skills/paper-pipeline/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/README.md +1 -1
- package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
- package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -5
- package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
- package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
- package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
- package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
- package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
- package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
- package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/tighten-paper/structure.mjs +1 -1
- package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
- package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
- package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
- package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
- package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
- package/src/CLAUDE.md +1 -1
- package/src/adapters/banal/index.test.ts +3 -6
- package/src/adapters/banal/index.ts +7 -4
- package/src/adapters/banal/invocation.test.ts +3 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +3 -3
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/curl/download.test.ts +4 -2
- package/src/adapters/node/files.io.ts +2 -2
- package/src/adapters/node/files.test.ts +3 -1
- package/src/adapters/node/process.io.ts +4 -3
- package/src/adapters/node/process.test.ts +1 -1
- package/src/adapters/node/workspace.test.ts +3 -1
- package/src/adapters/pdfjs/fill.test.ts +2 -2
- package/src/adapters/pdfjs/fill.ts +14 -7
- package/src/adapters/references/index.test.ts +31 -38
- package/src/adapters/references/index.ts +26 -58
- package/src/adapters/references/pool.ts +3 -5
- package/src/adapters/references/reach.io.ts +2 -1
- package/src/build-engine.harness.mjs +1 -1
- package/src/build-engine.test.ts +2 -2
- package/src/build-engine.ts +2 -2
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +15 -14
- package/src/build.ts +28 -19
- package/src/cli.harness.mjs +6 -6
- package/src/cli.test.ts +13 -6
- package/src/cli.ts +39 -37
- package/src/doctor.harness.mjs +3 -3
- package/src/doctor.ts +3 -2
- package/src/domain/lookup-cache.ts +21 -11
- package/src/domain/result.test.ts +2 -2
- package/src/domain/text.test.ts +32 -0
- package/src/domain/text.ts +23 -0
- package/src/engine.harness.mjs +2 -2
- package/src/engine.ts +4 -3
- package/src/facts-file.test.ts +9 -7
- package/src/facts-file.ts +2 -1
- package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
- package/src/hooks-settings.ts +20 -9
- package/src/init.test.ts +12 -9
- package/src/init.ts +22 -12
- package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
- package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
- package/src/latex-loop.ts +3 -3
- package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
- package/src/link-skills.test.ts +3 -3
- package/src/link-skills.ts +5 -4
- package/src/lint-fix.test.ts +4 -6
- package/src/linted-set.test.ts +2 -2
- package/src/new-paper.harness.mjs +2 -2
- package/src/new-paper.test.ts +8 -10
- package/src/new-paper.ts +2 -2
- package/src/paper-files.test.ts +3 -5
- package/src/paper-settings-commands.test.ts +5 -4
- package/src/paper-settings.test.ts +5 -5
- package/src/paper-settings.ts +8 -9
- package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
- package/src/pdf-facts.test.ts +10 -11
- package/src/pdf-facts.ts +2 -1
- package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
- package/src/pdf-geometry.ts +3 -1
- package/src/presets.ts +3 -2
- package/src/references.test.ts +51 -43
- package/src/references.ts +14 -7
- package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
- package/src/rules-config.ts +7 -4
- package/src/rules-off.test.ts +6 -4
- package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
- package/src/structure.ts +1 -1
- package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
- package/src/tex-requirements.ts +8 -5
- package/src/toolchain.harness.mjs +2 -2
- package/src/toolchain.test.ts +1 -1
- package/src/toolchain.ts +31 -17
- package/src/venue-rules.test.ts +62 -64
- package/src/venue-rules.ts +1 -1
- package/tsconfig.json +6 -3
- package/tsconfig.pkg.json +10 -0
- package/lib/check.d.mts +0 -17
- package/lib/paper-config.d.mts +0 -31
- package/scripts/layer-legacy-frozen.d.mts +0 -36
- package/scripts/rules-are-content-only.harness.mjs +0 -105
- package/scripts/rules-are-content-only.mjs +0 -148
- package/scripts/rules-are-content-only.test.mjs +0 -61
- package/scripts/test.mjs +0 -44
- package/scripts/test.test.mjs +0 -52
- /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
- /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
- /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
- /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
- /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
|
@@ -59,51 +59,87 @@
|
|
|
59
59
|
* is REPORTED WITHOUT A FIX and the zero is offered as a suggestion: an autofix must never be the
|
|
60
60
|
* one to decide what a number means. A real `.05` in any other paragraph is fixed as before.
|
|
61
61
|
*/
|
|
62
|
+
import assert from "node:assert/strict";
|
|
62
63
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
64
|
+
import type { Nodes } from "mdast";
|
|
65
|
+
import type { TexArg, TexNode, TexRoot } from "./latex-language.ts";
|
|
66
|
+
import type {
|
|
67
|
+
RuleContext,
|
|
68
|
+
RuleFixer,
|
|
69
|
+
RuleReport,
|
|
70
|
+
RuleSourceCode,
|
|
71
|
+
} from "./rule-context.ts";
|
|
72
|
+
|
|
73
|
+
/** Where a `.bib` embedded in a `filecontents` environment sits in the file. */
|
|
74
|
+
export interface BibRange {
|
|
75
|
+
readonly start: number;
|
|
76
|
+
readonly end: number;
|
|
77
|
+
readonly bodyStart: number;
|
|
78
|
+
readonly body: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The `.tex` language's source code: the one that carries the unprojected file. */
|
|
82
|
+
type TexSourceCode = RuleSourceCode & { readonly raw: string };
|
|
83
|
+
|
|
84
|
+
/** A run of visible text, and for each of its characters the offset it came from (or null). */
|
|
85
|
+
interface Run {
|
|
86
|
+
text: string;
|
|
87
|
+
offs: (number | null)[];
|
|
88
|
+
}
|
|
63
89
|
|
|
64
90
|
/** The inline bibliography — a `filecontents` block writing a `.bib` — with its offsets. */
|
|
65
|
-
export function bibRange(text) {
|
|
91
|
+
export function bibRange(text: string): BibRange | null {
|
|
66
92
|
const m =
|
|
67
93
|
/\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/d.exec(
|
|
68
94
|
text,
|
|
69
95
|
);
|
|
70
96
|
if (!m) return null;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
97
|
+
const [whole, body] = m;
|
|
98
|
+
const bodyStart = m.indices?.[1]?.[0];
|
|
99
|
+
// The `d` flag records every group's indices, and group 1 is not optional.
|
|
100
|
+
assert.ok(body !== undefined && bodyStart !== undefined, "no .bib body");
|
|
101
|
+
return { start: m.index, end: m.index + whole.length, bodyStart, body };
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
/**
|
|
80
105
|
* The ranges a markup rule must not read: LaTeX comments (the language gives them as `html`
|
|
81
106
|
* nodes, with positions) and the inline bibliography.
|
|
82
107
|
*/
|
|
83
|
-
function skippedRanges(
|
|
84
|
-
|
|
108
|
+
function skippedRanges(
|
|
109
|
+
sourceCode: RuleSourceCode,
|
|
110
|
+
raw: string,
|
|
111
|
+
): [number, number][] {
|
|
112
|
+
const out: [number, number][] = [];
|
|
85
113
|
for (const n of sourceCode.ast?.children ?? [])
|
|
86
|
-
if (n.type === "html" && n.position)
|
|
87
|
-
|
|
114
|
+
if (n.type === "html" && n.position) {
|
|
115
|
+
// An offset-less point compares as NaN: inside no range, as `undefined` always did.
|
|
116
|
+
const {
|
|
117
|
+
start: { offset: s = Number.NaN },
|
|
118
|
+
end: { offset: e = Number.NaN },
|
|
119
|
+
} = n.position;
|
|
120
|
+
out.push([s, e]);
|
|
121
|
+
}
|
|
88
122
|
const bib = bibRange(raw);
|
|
89
123
|
if (bib) out.push([bib.start, bib.end]);
|
|
90
124
|
return out;
|
|
91
125
|
}
|
|
92
|
-
const inside = (ranges
|
|
126
|
+
const inside = (ranges: readonly [number, number][], i: number) =>
|
|
127
|
+
ranges.some(([s, e]) => i >= s && i < e);
|
|
93
128
|
|
|
94
129
|
/** Every match of `re` in `raw` outside the skipped ranges. */
|
|
95
|
-
function markupMatches(sourceCode, re) {
|
|
130
|
+
function markupMatches(sourceCode: TexSourceCode, re: RegExp) {
|
|
96
131
|
const raw = sourceCode.raw;
|
|
97
132
|
const skip = skippedRanges(sourceCode, raw);
|
|
98
133
|
return [...raw.matchAll(re)].filter((m) => !inside(skip, m.index));
|
|
99
134
|
}
|
|
100
135
|
|
|
101
|
-
const isTex = (sourceCode)
|
|
136
|
+
const isTex = (sourceCode: RuleSourceCode): sourceCode is TexSourceCode =>
|
|
137
|
+
typeof sourceCode.raw === "string";
|
|
102
138
|
|
|
103
139
|
// ── the text a READER sees, with the source offset of every character ─────────────────────
|
|
104
140
|
|
|
105
141
|
/** A run of visible text; `offs[i]` is the source offset of `text[i]`, or null when unknown. */
|
|
106
|
-
const run = () => ({ text: "", offs: [] });
|
|
142
|
+
const run = (): Run => ({ text: "", offs: [] });
|
|
107
143
|
|
|
108
144
|
// Environments whose body is not typeset as text: code, drawings, the inline bibliography.
|
|
109
145
|
const TEX_HIDDEN_ENV =
|
|
@@ -117,7 +153,7 @@ const TEX_LAST_ARG = /^(multicolumn|multirow|textcolor|href)$/;
|
|
|
117
153
|
const TEX_MATH_HIDDEN =
|
|
118
154
|
/^(label|ref|eqref|autoref|cite|hspace|vspace|hskip|vskip|kern|mkern|mskip|rule|phantom|hphantom|vphantom|color|raisebox|includegraphics)$/;
|
|
119
155
|
|
|
120
|
-
const mandatory = (macro) =>
|
|
156
|
+
const mandatory = (macro: { readonly args?: readonly TexArg[] }) =>
|
|
121
157
|
(macro.args || []).filter((a) => a.openMark === "{");
|
|
122
158
|
|
|
123
159
|
/**
|
|
@@ -125,15 +161,15 @@ const mandatory = (macro) =>
|
|
|
125
161
|
* arguments come back as the NEXT SIBLINGS: an optional `[…]` as bare strings, then groups.
|
|
126
162
|
* They are parameters, not prose. Returns the index of the last sibling to skip after `i`.
|
|
127
163
|
*/
|
|
128
|
-
|
|
164
|
+
/** Whether `n` is a string node whose text passes `test`. */
|
|
165
|
+
const isString = (n: TexNode | undefined, test: (s: string) => boolean) =>
|
|
166
|
+
n?.type === "string" && test(n.content);
|
|
167
|
+
|
|
168
|
+
function unparsedArgsEnd(nodes: readonly TexNode[], i: number): number {
|
|
129
169
|
let j = i + 1;
|
|
130
|
-
if (nodes[j]
|
|
170
|
+
if (isString(nodes[j], (c) => c.startsWith("["))) {
|
|
131
171
|
let k = j;
|
|
132
|
-
while (
|
|
133
|
-
k < nodes.length &&
|
|
134
|
-
!(nodes[k].type === "string" && nodes[k].content.endsWith("]"))
|
|
135
|
-
)
|
|
136
|
-
k++;
|
|
172
|
+
while (k < nodes.length && !isString(nodes[k], (c) => c.endsWith("]"))) k++;
|
|
137
173
|
if (k < nodes.length) j = k + 1;
|
|
138
174
|
}
|
|
139
175
|
while (nodes[j]?.type === "group") j++;
|
|
@@ -141,13 +177,22 @@ function unparsedArgsEnd(nodes, i) {
|
|
|
141
177
|
}
|
|
142
178
|
|
|
143
179
|
/** Append a string node to the current run, keeping each character's offset. */
|
|
144
|
-
function appendString(
|
|
180
|
+
function appendString(
|
|
181
|
+
cur: Run,
|
|
182
|
+
n: {
|
|
183
|
+
readonly content: string;
|
|
184
|
+
readonly position?: { readonly start?: { readonly offset: number } };
|
|
185
|
+
},
|
|
186
|
+
src: string,
|
|
187
|
+
) {
|
|
145
188
|
const start = n.position?.start?.offset;
|
|
146
189
|
const exact =
|
|
147
190
|
typeof start === "number" &&
|
|
148
|
-
src.slice(start, start + n.content.length) === n.content
|
|
191
|
+
src.slice(start, start + n.content.length) === n.content
|
|
192
|
+
? start
|
|
193
|
+
: null;
|
|
149
194
|
for (let i = 0; i < n.content.length; i++)
|
|
150
|
-
cur.offs.push(exact ?
|
|
195
|
+
cur.offs.push(exact === null ? null : exact + i);
|
|
151
196
|
cur.text += n.content;
|
|
152
197
|
}
|
|
153
198
|
|
|
@@ -156,14 +201,23 @@ function appendString(cur, n, src) {
|
|
|
156
201
|
* `string` nodes with nothing between them: math splits `.05` into `.`, `0`, `5`, and prose
|
|
157
202
|
* keeps `2310.05736` whole — both come back as one word. Anything else ends the run.
|
|
158
203
|
*/
|
|
159
|
-
|
|
204
|
+
/** What stands in for an index past the end: a node every branch below passes over. */
|
|
205
|
+
const NOTHING: TexNode = { type: "whitespace" };
|
|
206
|
+
|
|
207
|
+
function texRuns(
|
|
208
|
+
nodes: readonly TexNode[] | undefined,
|
|
209
|
+
math: boolean,
|
|
210
|
+
src: string,
|
|
211
|
+
out: Run[],
|
|
212
|
+
): Run[] {
|
|
160
213
|
let cur = run();
|
|
161
214
|
const flush = () => {
|
|
162
215
|
if (cur.text) out.push(cur);
|
|
163
216
|
cur = run();
|
|
164
217
|
};
|
|
165
|
-
|
|
166
|
-
|
|
218
|
+
const list = nodes || [];
|
|
219
|
+
for (let i = 0; i < list.length; i++) {
|
|
220
|
+
const { [i]: n = NOTHING } = list;
|
|
167
221
|
if (n.type === "string") {
|
|
168
222
|
appendString(cur, n, src);
|
|
169
223
|
continue;
|
|
@@ -192,9 +246,9 @@ function texRuns(nodes, math, src, out) {
|
|
|
192
246
|
} else if (TEX_PROSE_ARG.test(n.content)) {
|
|
193
247
|
for (const a of args) texRuns(a.content, false, src, out);
|
|
194
248
|
} else if (TEX_LAST_ARG.test(n.content) && args.length) {
|
|
195
|
-
texRuns(args
|
|
249
|
+
texRuns(args.at(-1)?.content, false, src, out);
|
|
196
250
|
}
|
|
197
|
-
if (!math && !(n.args || []).length) i = unparsedArgsEnd(
|
|
251
|
+
if (!math && !(n.args || []).length) i = unparsedArgsEnd(list, i);
|
|
198
252
|
}
|
|
199
253
|
}
|
|
200
254
|
flush();
|
|
@@ -206,13 +260,16 @@ function texRuns(nodes, math, src, out) {
|
|
|
206
260
|
* with the fields the walk tolerates being absent.
|
|
207
261
|
*/
|
|
208
262
|
export function texVisibleRuns(
|
|
209
|
-
raw,
|
|
210
|
-
{
|
|
211
|
-
)
|
|
263
|
+
raw: string,
|
|
264
|
+
{
|
|
265
|
+
parse = (s: string): TexRoot => getParser().parse(s),
|
|
266
|
+
}: { parse?: (s: string) => TexRoot } = {},
|
|
267
|
+
): Run[] {
|
|
212
268
|
const root = parse(raw).content;
|
|
213
269
|
// Only the document body is typeset. A fragment with no `document` environment is all body.
|
|
214
270
|
const document = root.find(
|
|
215
|
-
(n)
|
|
271
|
+
(n): n is Extract<TexNode, { type: "environment" | "mathenv" }> =>
|
|
272
|
+
n.type === "environment" && n.env === "document",
|
|
216
273
|
);
|
|
217
274
|
return texRuns(document ? document.content : root, false, raw, []);
|
|
218
275
|
}
|
|
@@ -232,8 +289,8 @@ const MD_HIDDEN = new Set([
|
|
|
232
289
|
* markdown's escapes (`\*`) and character references (`<`) resolved, so the two are walked
|
|
233
290
|
* side by side; a character whose source cannot be followed gets null (reported, not fixed).
|
|
234
291
|
*/
|
|
235
|
-
function mdOffsets(value, src, start) {
|
|
236
|
-
const offs = [];
|
|
292
|
+
function mdOffsets(value: string, src: string, start: number) {
|
|
293
|
+
const offs: (number | null)[] = [];
|
|
237
294
|
let j = start;
|
|
238
295
|
for (const ch of value) {
|
|
239
296
|
if (src[j] === ch) offs.push(j++);
|
|
@@ -251,29 +308,39 @@ function mdOffsets(value, src, start) {
|
|
|
251
308
|
return offs;
|
|
252
309
|
}
|
|
253
310
|
|
|
254
|
-
function mdVisibleRuns(
|
|
311
|
+
function mdVisibleRuns(
|
|
312
|
+
node: Nodes | undefined,
|
|
313
|
+
src: string,
|
|
314
|
+
out: Run[] = [],
|
|
315
|
+
): Run[] {
|
|
255
316
|
if (!node || MD_HIDDEN.has(node.type)) return out;
|
|
256
317
|
if (node.type === "text") {
|
|
257
|
-
const start = node.position?.start
|
|
318
|
+
const start = node.position?.start.offset;
|
|
258
319
|
out.push({
|
|
259
320
|
text: node.value,
|
|
260
321
|
offs:
|
|
261
322
|
typeof start === "number"
|
|
262
323
|
? mdOffsets(node.value, src, start)
|
|
263
|
-
:
|
|
324
|
+
: Array.from(node.value, () => null),
|
|
264
325
|
});
|
|
265
326
|
}
|
|
266
|
-
for (const c of node.children
|
|
327
|
+
for (const c of "children" in node ? node.children : [])
|
|
328
|
+
mdVisibleRuns(c, src, out);
|
|
267
329
|
return out;
|
|
268
330
|
}
|
|
269
331
|
|
|
270
|
-
const visibleRuns = (sourceCode) =>
|
|
332
|
+
const visibleRuns = (sourceCode: RuleSourceCode): Run[] =>
|
|
271
333
|
isTex(sourceCode)
|
|
272
334
|
? texVisibleRuns(sourceCode.raw)
|
|
273
335
|
: mdVisibleRuns(sourceCode.ast, sourceCode.text);
|
|
274
336
|
|
|
275
337
|
/** A report at `[from, to)` of the file. */
|
|
276
|
-
const at = (
|
|
338
|
+
const at = (
|
|
339
|
+
context: RuleContext,
|
|
340
|
+
from: number,
|
|
341
|
+
to: number,
|
|
342
|
+
rest: Omit<RuleReport, "loc" | "node">,
|
|
343
|
+
) => {
|
|
277
344
|
const sc = context.sourceCode;
|
|
278
345
|
context.report({
|
|
279
346
|
loc: { start: sc.getLocFromIndex(from), end: sc.getLocFromIndex(to) },
|
|
@@ -288,14 +355,14 @@ const at = (context, from, to, rest) => {
|
|
|
288
355
|
const SECTION_TEX = /(§|\\S(?![A-Za-z]))(~|[ \t]*)(?=(\\(?:auto)?ref\b)|(\d))/g;
|
|
289
356
|
const SECTION_GLYPH = /§/g;
|
|
290
357
|
|
|
291
|
-
function sectionWord(context) {
|
|
358
|
+
function sectionWord(context: RuleContext) {
|
|
292
359
|
const sc = context.sourceCode;
|
|
293
360
|
if (isTex(sc)) {
|
|
294
361
|
const seen = new Set();
|
|
295
362
|
for (const m of markupMatches(sc, SECTION_TEX)) {
|
|
296
363
|
seen.add(m.index);
|
|
297
364
|
const word = m[3] ? "Section~" : "Section ";
|
|
298
|
-
const range = [m.index, m.index + m[0].length];
|
|
365
|
+
const range: [number, number] = [m.index, m.index + m[0].length];
|
|
299
366
|
at(context, range[0], range[1], {
|
|
300
367
|
messageId: "sign",
|
|
301
368
|
fix: (f) => f.replaceTextRange(range, word),
|
|
@@ -313,7 +380,8 @@ function sectionWord(context) {
|
|
|
313
380
|
const from = r.offs[m.index];
|
|
314
381
|
if (from === null || from === undefined) continue;
|
|
315
382
|
// The sign and the space after it; the number stays.
|
|
316
|
-
const
|
|
383
|
+
const [, gap = ""] = m;
|
|
384
|
+
const range: [number, number] = [from, from + 1 + gap.length];
|
|
317
385
|
at(context, range[0], range[1], {
|
|
318
386
|
messageId: "sign",
|
|
319
387
|
...(m[2] ? { fix: (f) => f.replaceTextRange(range, "Section ") } : {}),
|
|
@@ -334,13 +402,13 @@ const DESIGNATOR_BEFORE = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*$/;
|
|
|
334
402
|
const DESIGNATED = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*\.\d/;
|
|
335
403
|
|
|
336
404
|
/** The source of the paragraph holding `i`, up to `i`: from the last blank line before it. */
|
|
337
|
-
const paragraphBefore = (src, i) => {
|
|
405
|
+
const paragraphBefore = (src: string, i: number) => {
|
|
338
406
|
const head = src.slice(0, i);
|
|
339
407
|
const blank = [...head.matchAll(/\n[ \t]*\n/g)].at(-1);
|
|
340
408
|
return head.slice(blank ? blank.index + blank[0].length : 0);
|
|
341
409
|
};
|
|
342
410
|
|
|
343
|
-
function leadingZero(context) {
|
|
411
|
+
function leadingZero(context: RuleContext) {
|
|
344
412
|
const sc = context.sourceCode;
|
|
345
413
|
const src = isTex(sc) ? sc.raw : sc.text;
|
|
346
414
|
for (const r of visibleRuns(sc))
|
|
@@ -349,7 +417,7 @@ function leadingZero(context) {
|
|
|
349
417
|
if (dot === null || dot === undefined) continue;
|
|
350
418
|
const before = paragraphBefore(src, dot);
|
|
351
419
|
if (DESIGNATOR_BEFORE.test(before)) continue;
|
|
352
|
-
const zero = (f) => f.insertTextBeforeRange([dot, dot], "0");
|
|
420
|
+
const zero = (f: RuleFixer) => f.insertTextBeforeRange([dot, dot], "0");
|
|
353
421
|
at(
|
|
354
422
|
context,
|
|
355
423
|
dot,
|
|
@@ -371,7 +439,7 @@ function leadingZero(context) {
|
|
|
371
439
|
|
|
372
440
|
const FIG_REF = /\b(Fig\.|Figure)(?=~?\\(?:ref|autoref)\b)/g;
|
|
373
441
|
|
|
374
|
-
function figureRefStyle(context) {
|
|
442
|
+
function figureRefStyle(context: RuleContext) {
|
|
375
443
|
const sc = context.sourceCode;
|
|
376
444
|
if (!isTex(sc)) return;
|
|
377
445
|
const all = markupMatches(sc, FIG_REF);
|
|
@@ -382,16 +450,22 @@ function figureRefStyle(context) {
|
|
|
382
450
|
const [minority, word] =
|
|
383
451
|
short.length > long.length ? [long, "Fig."] : [short, "Figure"];
|
|
384
452
|
for (const m of minority) {
|
|
385
|
-
const
|
|
453
|
+
const [, form = ""] = m;
|
|
454
|
+
const range: [number, number] = [m.index, m.index + form.length];
|
|
386
455
|
at(context, range[0], range[1], {
|
|
387
456
|
messageId: "mixed",
|
|
388
|
-
data: { form
|
|
457
|
+
data: { form, word, n: String(all.length - minority.length) },
|
|
389
458
|
fix: (f) => f.replaceTextRange(range, word),
|
|
390
459
|
});
|
|
391
460
|
}
|
|
392
461
|
}
|
|
393
462
|
|
|
394
|
-
const rule = (
|
|
463
|
+
const rule = (
|
|
464
|
+
description: string,
|
|
465
|
+
messages: Readonly<Record<string, string>>,
|
|
466
|
+
check: (context: RuleContext) => void,
|
|
467
|
+
meta: { readonly hasSuggestions?: boolean } = {},
|
|
468
|
+
) => ({
|
|
395
469
|
meta: {
|
|
396
470
|
type: "suggestion",
|
|
397
471
|
fixable: "code",
|
|
@@ -400,7 +474,11 @@ const rule = (description, messages, check, meta = {}) => ({
|
|
|
400
474
|
messages,
|
|
401
475
|
...meta,
|
|
402
476
|
},
|
|
403
|
-
create: (context
|
|
477
|
+
create: (context: RuleContext) => ({
|
|
478
|
+
"root:exit": () => {
|
|
479
|
+
check(context);
|
|
480
|
+
},
|
|
481
|
+
}),
|
|
404
482
|
});
|
|
405
483
|
|
|
406
484
|
export default {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* not throw" as success; an exported `tests` object is run by nothing.
|
|
18
18
|
*/
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { PAPERS_DIR_FIELD } from "../lib/paper-config.
|
|
20
|
+
import { PAPERS_DIR_FIELD } from "../lib/paper-config.ts";
|
|
21
21
|
import {
|
|
22
22
|
mkdirSync,
|
|
23
23
|
mkdtempSync,
|
|
@@ -31,7 +31,7 @@ import { tmpdir } from "node:os";
|
|
|
31
31
|
import { dirname, isAbsolute, join } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
|
|
34
|
-
import { DEFAULT_PAPERS_ROOT, paperFiles, papersRoot } from "./papers.
|
|
34
|
+
import { DEFAULT_PAPERS_ROOT, paperFiles, papersRoot } from "./papers.ts";
|
|
35
35
|
|
|
36
36
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
37
|
const ROOT = join(HERE, "..");
|
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
DEFAULT_PAPERS_ROOT,
|
|
83
83
|
PAPERS_DIR_FIELD,
|
|
84
84
|
CONFIG_FILE,
|
|
85
|
-
} from "../lib/paper-config.
|
|
85
|
+
} from "../lib/paper-config.ts";
|
|
86
86
|
export { DEFAULT_PAPERS_ROOT };
|
|
87
87
|
|
|
88
88
|
/**
|
|
@@ -98,7 +98,10 @@ export { DEFAULT_PAPERS_ROOT };
|
|
|
98
98
|
* globs, which ESLint interprets relative to the config, and an absolute path there
|
|
99
99
|
* would change their meaning.
|
|
100
100
|
*/
|
|
101
|
-
export function papersRoot(
|
|
101
|
+
export function papersRoot(
|
|
102
|
+
settings: Readonly<Record<string, unknown>> | null | undefined,
|
|
103
|
+
baseDir: string = process.cwd(),
|
|
104
|
+
): string {
|
|
102
105
|
const declared = settings?.[PAPERS_DIR_FIELD];
|
|
103
106
|
// 🔴 `declared === undefined`, NOT `declared ?? DEFAULT`. The two differ on exactly one
|
|
104
107
|
// input — `"papersDir": null` — and the difference is the whole point: `??` reads an explicit
|
|
@@ -118,7 +121,7 @@ export function papersRoot(settings, baseDir = process.cwd()) {
|
|
|
118
121
|
? `Nothing was declared, so the default "${DEFAULT_PAPERS_ROOT}" was used. Declare the ` +
|
|
119
122
|
`real location in ${CONFIG_FILE}:\n` +
|
|
120
123
|
` { "${PAPERS_DIR_FIELD}": "path/to/papers" }`
|
|
121
|
-
: `It is declared in ${CONFIG_FILE} as "${
|
|
124
|
+
: `It is declared in ${CONFIG_FILE} as "${root}". Fix it there, or create the ` +
|
|
122
125
|
`directory.`) +
|
|
123
126
|
`\nThis is thrown rather than ignored on purpose: a papers root that matches nothing ` +
|
|
124
127
|
`makes every rule lint zero files, and a run with zero findings is indistinguishable ` +
|
|
@@ -136,7 +139,7 @@ export function papersRoot(settings, baseDir = process.cwd()) {
|
|
|
136
139
|
* with the rule, in the same package, rather than as a line every consumer has to add by hand
|
|
137
140
|
* to keep up. A hand-maintained list of paths is the thing that rots, and rots silently.
|
|
138
141
|
*/
|
|
139
|
-
export function paperFiles(root) {
|
|
142
|
+
export function paperFiles(root: string) {
|
|
140
143
|
return {
|
|
141
144
|
/** Markdown drafts — the two names a paper's prose is allowed to have. */
|
|
142
145
|
md: [`${root}/*/paper.md`, `${root}/*/draft.md`],
|
|
@@ -23,12 +23,12 @@ import { tmpdir } from "node:os";
|
|
|
23
23
|
import { dirname, join } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { ESLint } from "eslint";
|
|
26
|
-
import { texLanguage } from "./latex-language.
|
|
27
|
-
import { createChecker } from "../lib/check.
|
|
26
|
+
import { texLanguage } from "./latex-language.ts";
|
|
27
|
+
import { createChecker } from "../lib/check.ts";
|
|
28
28
|
|
|
29
29
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
30
|
const { parseFacts, judgeColumns, DEFAULT_TOLERANCE_PT } = await import(
|
|
31
|
-
join(HERE, "pdf-last-page-balance.
|
|
31
|
+
join(HERE, "pdf-last-page-balance.ts")
|
|
32
32
|
);
|
|
33
33
|
const { buildConfig } = await import(join(HERE, "..", "src", "cli.ts"));
|
|
34
34
|
|
|
@@ -35,7 +35,58 @@
|
|
|
35
35
|
import { createHash } from "node:crypto";
|
|
36
36
|
import { existsSync, readFileSync } from "node:fs";
|
|
37
37
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
38
|
-
import
|
|
38
|
+
import assert from "node:assert/strict";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
import { CONFIG_FILE } from "../lib/paper-config.ts";
|
|
41
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
42
|
+
|
|
43
|
+
/** A reason to report, in ESLint's terms. */
|
|
44
|
+
export interface Finding {
|
|
45
|
+
readonly messageId: string;
|
|
46
|
+
readonly data: Readonly<Record<string, string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The `last_page` field, as far as this rule can judge it. */
|
|
50
|
+
export type LastPage =
|
|
51
|
+
| { readonly kind: "stub" | "review" }
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "measured";
|
|
54
|
+
readonly left: number;
|
|
55
|
+
readonly right: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** What `parseFacts` reads out of `paper.facts.json`, or why it cannot. */
|
|
59
|
+
export type ParsedFacts =
|
|
60
|
+
| {
|
|
61
|
+
readonly ok: true;
|
|
62
|
+
readonly pdf: string;
|
|
63
|
+
/** Whatever the file holds there; it matched the digest pattern, and is compared as-is. */
|
|
64
|
+
readonly sha: unknown;
|
|
65
|
+
readonly last: LastPage;
|
|
66
|
+
}
|
|
67
|
+
| ({ readonly ok: false } & Finding);
|
|
68
|
+
|
|
69
|
+
// The fields read, each as it is written; anything that is not a mapping has none of them.
|
|
70
|
+
const FactsFields = z
|
|
71
|
+
.looseObject({
|
|
72
|
+
schema: z.unknown().optional(),
|
|
73
|
+
pdf: z.unknown().optional(),
|
|
74
|
+
pdf_sha256: z.unknown().optional(),
|
|
75
|
+
last_page: z.unknown().optional(),
|
|
76
|
+
})
|
|
77
|
+
.catch({});
|
|
78
|
+
const LastPageFields = z
|
|
79
|
+
.looseObject({
|
|
80
|
+
kind: z.unknown().optional(),
|
|
81
|
+
columns_pt: z.unknown().optional(),
|
|
82
|
+
})
|
|
83
|
+
.catch({});
|
|
84
|
+
/** Two column heights: finite numbers, exactly two of them. */
|
|
85
|
+
const Columns = z.tuple([z.number(), z.number()]);
|
|
86
|
+
const Settings = z.looseObject({ extends: z.unknown().optional() }).catch({});
|
|
87
|
+
const RuleOptions = z
|
|
88
|
+
.object({ tolerancePt: z.number().optional() })
|
|
89
|
+
.default({});
|
|
39
90
|
|
|
40
91
|
/** The difference, in points, that two columns may end apart. See the harness for why 120. */
|
|
41
92
|
export const DEFAULT_TOLERANCE_PT = 120;
|
|
@@ -43,44 +94,40 @@ export const DEFAULT_TOLERANCE_PT = 120;
|
|
|
43
94
|
export const FACTS_SCHEMA = 2;
|
|
44
95
|
export const FACTS_REL = join("_build", "paper.facts.json");
|
|
45
96
|
|
|
46
|
-
const isHeight = (x) => typeof x === "number" && Number.isFinite(x);
|
|
47
|
-
|
|
48
97
|
/** The `last_page` field, parsed. */
|
|
49
|
-
function lastPageOf(v) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
c.length === 2 &&
|
|
56
|
-
c.every(isHeight)
|
|
57
|
-
)
|
|
58
|
-
return { kind: "measured", left: c[0], right: c[1] };
|
|
98
|
+
function lastPageOf(v: unknown): LastPage | null {
|
|
99
|
+
const { kind, columns_pt } = LastPageFields.parse(v);
|
|
100
|
+
if (kind === "stub" || kind === "review") return { kind };
|
|
101
|
+
const c = Columns.safeParse(columns_pt);
|
|
102
|
+
if (kind === "measured" && c.success)
|
|
103
|
+
return { kind: "measured", left: c.data[0], right: c.data[1] };
|
|
59
104
|
return null;
|
|
60
105
|
}
|
|
61
106
|
|
|
62
107
|
/**
|
|
63
108
|
* The facts file's text → what this rule needs, or the reason it cannot be judged. Pure.
|
|
64
|
-
*
|
|
65
|
-
* @returns {{ ok: true, pdf: string, sha: string, last: object } | { ok: false, messageId: string, data: object }}
|
|
66
109
|
*/
|
|
67
|
-
export function parseFacts(text) {
|
|
68
|
-
let
|
|
110
|
+
export function parseFacts(text: string): ParsedFacts {
|
|
111
|
+
let raw: unknown;
|
|
69
112
|
try {
|
|
70
|
-
|
|
113
|
+
raw = JSON.parse(text);
|
|
71
114
|
} catch (e) {
|
|
115
|
+
// `JSON.parse` throws a `SyntaxError` and nothing else.
|
|
116
|
+
assert.ok(e instanceof Error);
|
|
72
117
|
return { ok: false, messageId: "factsBroken", data: { why: e.message } };
|
|
73
118
|
}
|
|
74
|
-
|
|
119
|
+
const d = FactsFields.parse(raw);
|
|
120
|
+
if (d.schema !== FACTS_SCHEMA)
|
|
75
121
|
return {
|
|
76
122
|
ok: false,
|
|
77
123
|
messageId: "schema",
|
|
78
|
-
data: { got: JSON.stringify(d
|
|
124
|
+
data: { got: JSON.stringify(d.schema ?? null) },
|
|
79
125
|
};
|
|
80
126
|
const last = lastPageOf(d.last_page);
|
|
81
127
|
if (
|
|
82
128
|
typeof d.pdf !== "string" ||
|
|
83
|
-
|
|
129
|
+
typeof d.pdf_sha256 !== "string" ||
|
|
130
|
+
!/^[0-9a-f]{64}$/.test(d.pdf_sha256) ||
|
|
84
131
|
!last
|
|
85
132
|
)
|
|
86
133
|
return {
|
|
@@ -94,9 +141,12 @@ export function parseFacts(text) {
|
|
|
94
141
|
}
|
|
95
142
|
|
|
96
143
|
/** Whether the facts describe the PDF on disk: null when they do, else the reason. */
|
|
97
|
-
function staleness(
|
|
144
|
+
function staleness(
|
|
145
|
+
paperDir: string,
|
|
146
|
+
facts: { readonly pdf: string; readonly sha: unknown },
|
|
147
|
+
): Finding | null {
|
|
98
148
|
const pdf = isAbsolute(facts.pdf) ? facts.pdf : join(paperDir, facts.pdf);
|
|
99
|
-
let got;
|
|
149
|
+
let got: string;
|
|
100
150
|
try {
|
|
101
151
|
got = createHash("sha256").update(readFileSync(pdf)).digest("hex");
|
|
102
152
|
} catch {
|
|
@@ -108,7 +158,10 @@ function staleness(paperDir, facts) {
|
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
/** The finding for a measured page, or null when it is balanced. Pure. */
|
|
111
|
-
export function judgeColumns(
|
|
161
|
+
export function judgeColumns(
|
|
162
|
+
last: LastPage,
|
|
163
|
+
tolerancePt: number,
|
|
164
|
+
): Finding | null {
|
|
112
165
|
if (last.kind !== "measured") return null;
|
|
113
166
|
const diff = Math.abs(last.left - last.right);
|
|
114
167
|
if (diff <= tolerancePt) return null;
|
|
@@ -124,7 +177,7 @@ export function judgeColumns(last, tolerancePt) {
|
|
|
124
177
|
}
|
|
125
178
|
|
|
126
179
|
/** Where a finding goes: the `\documentclass` line, where the class options that fix it live. */
|
|
127
|
-
function reportLine(file) {
|
|
180
|
+
function reportLine(file: string): number {
|
|
128
181
|
try {
|
|
129
182
|
const lines = readFileSync(file, "utf8").split("\n");
|
|
130
183
|
const i = lines.findIndex((l) => /^\s*\\documentclass\b/.test(l));
|
|
@@ -140,17 +193,19 @@ function reportLine(file) {
|
|
|
140
193
|
* repeating it as an error would fail a lint-only CI for every paper of a venue whose preset turns
|
|
141
194
|
* this rule on.
|
|
142
195
|
*/
|
|
143
|
-
function extendsPreset(paperDir) {
|
|
196
|
+
function extendsPreset(paperDir: string): boolean {
|
|
144
197
|
try {
|
|
145
|
-
const s =
|
|
146
|
-
|
|
198
|
+
const s = Settings.parse(
|
|
199
|
+
JSON.parse(readFileSync(join(paperDir, CONFIG_FILE), "utf8")),
|
|
200
|
+
);
|
|
201
|
+
return typeof s.extends === "string" && s.extends !== "";
|
|
147
202
|
} catch {
|
|
148
203
|
return false;
|
|
149
204
|
}
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
/** Everything the rule decides for one paper, as a finding or null. */
|
|
153
|
-
function verdict(paperDir, tolerancePt) {
|
|
208
|
+
function verdict(paperDir: string, tolerancePt: number): Finding | null {
|
|
154
209
|
const factsFile = join(paperDir, FACTS_REL);
|
|
155
210
|
if (!existsSync(factsFile))
|
|
156
211
|
return extendsPreset(paperDir)
|
|
@@ -204,10 +259,12 @@ const rule = {
|
|
|
204
259
|
"differs), so judging it would judge an earlier build — rebuild the paper",
|
|
205
260
|
},
|
|
206
261
|
},
|
|
207
|
-
create(context) {
|
|
262
|
+
create(context: RuleContext) {
|
|
208
263
|
// Judged once per paper, on its paper.tex: the paper directory is the file's directory.
|
|
209
264
|
if (basename(context.filename) !== "paper.tex") return {};
|
|
210
|
-
const tolerancePt =
|
|
265
|
+
const { tolerancePt = DEFAULT_TOLERANCE_PT } = RuleOptions.parse(
|
|
266
|
+
context.options[0],
|
|
267
|
+
);
|
|
211
268
|
return {
|
|
212
269
|
root() {
|
|
213
270
|
const v = verdict(dirname(context.filename), tolerancePt);
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { ESLint } from "eslint";
|
|
7
7
|
import markdown from "@eslint/markdown";
|
|
8
|
-
import review, { errorsOf } from "./review-frontmatter.
|
|
9
|
-
import sibling from "./sibling-frontmatter.
|
|
8
|
+
import review, { errorsOf } from "./review-frontmatter.ts";
|
|
9
|
+
import sibling from "./sibling-frontmatter.ts";
|
|
10
10
|
|
|
11
11
|
async function lint(text) {
|
|
12
12
|
const eslint = new ESLint({
|