paperlint 3.1.2 → 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/skills/verify-citations/scripts/verify-cites.mjs +11 -1
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +35 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +14 -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
|
@@ -106,8 +106,130 @@ import {
|
|
|
106
106
|
ConfigCommentParser,
|
|
107
107
|
Directive,
|
|
108
108
|
} from "@eslint/plugin-kit";
|
|
109
|
+
import assert from "node:assert/strict";
|
|
110
|
+
import type {
|
|
111
|
+
DirectiveType,
|
|
112
|
+
File,
|
|
113
|
+
FileProblem,
|
|
114
|
+
Language,
|
|
115
|
+
LanguageOptions,
|
|
116
|
+
OkParseResult,
|
|
117
|
+
ParseResult,
|
|
118
|
+
} from "@eslint/core";
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
/** A position in the `.tex`, as unified-latex and mdast both spell it. */
|
|
121
|
+
export interface Point {
|
|
122
|
+
readonly offset: number;
|
|
123
|
+
readonly line: number;
|
|
124
|
+
readonly column: number;
|
|
125
|
+
}
|
|
126
|
+
export interface Span {
|
|
127
|
+
readonly start: Point;
|
|
128
|
+
readonly end: Point;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A node as the projection reads it: unified-latex's shape, with every part the projection can
|
|
133
|
+
* live without made optional — a macro without `args`, an argument without `content`, an `env`
|
|
134
|
+
* given as nodes. unified-latex's own `Ast.Root` is one of these.
|
|
135
|
+
*/
|
|
136
|
+
export type TexNode =
|
|
137
|
+
| {
|
|
138
|
+
readonly type: "string" | "comment";
|
|
139
|
+
readonly content: string;
|
|
140
|
+
readonly position?: Span;
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
readonly type: "macro";
|
|
144
|
+
readonly content: string;
|
|
145
|
+
readonly args?: readonly TexArg[];
|
|
146
|
+
readonly position?: Span;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
readonly type: "environment" | "mathenv";
|
|
150
|
+
readonly env: string | readonly TexNode[];
|
|
151
|
+
readonly content?: readonly TexNode[];
|
|
152
|
+
readonly args?: readonly TexArg[];
|
|
153
|
+
readonly position?: Span;
|
|
154
|
+
}
|
|
155
|
+
| {
|
|
156
|
+
readonly type: "verbatim" | "verb";
|
|
157
|
+
readonly env: string;
|
|
158
|
+
readonly content: string;
|
|
159
|
+
readonly position?: Span;
|
|
160
|
+
}
|
|
161
|
+
| {
|
|
162
|
+
readonly type: "group" | "inlinemath" | "displaymath" | "root";
|
|
163
|
+
readonly content: readonly TexNode[];
|
|
164
|
+
readonly position?: Span;
|
|
165
|
+
}
|
|
166
|
+
| { readonly type: "whitespace" | "parbreak"; readonly position?: Span }
|
|
167
|
+
| TexArg;
|
|
168
|
+
|
|
169
|
+
/** A macro's or an environment's argument. */
|
|
170
|
+
export interface TexArg {
|
|
171
|
+
readonly type: "argument";
|
|
172
|
+
readonly content?: readonly TexNode[];
|
|
173
|
+
readonly openMark?: string;
|
|
174
|
+
readonly closeMark?: string;
|
|
175
|
+
readonly position?: Span;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** What a parser hands the projection. */
|
|
179
|
+
export interface TexRoot {
|
|
180
|
+
readonly content: readonly TexNode[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The projection's mdast-shaped output: the node kinds the prose rules read (`root`, `heading`,
|
|
185
|
+
* `text`, `strong`, `code`, `html`), each with its span in the `.tex`.
|
|
186
|
+
*/
|
|
187
|
+
export type MdNode =
|
|
188
|
+
| {
|
|
189
|
+
readonly type: "root" | "strong";
|
|
190
|
+
readonly position: Span;
|
|
191
|
+
readonly children: readonly MdNode[];
|
|
192
|
+
}
|
|
193
|
+
| {
|
|
194
|
+
readonly type: "heading";
|
|
195
|
+
readonly position: Span;
|
|
196
|
+
readonly depth: number;
|
|
197
|
+
readonly title: string;
|
|
198
|
+
readonly children: readonly MdNode[];
|
|
199
|
+
}
|
|
200
|
+
| {
|
|
201
|
+
readonly type: "text" | "html";
|
|
202
|
+
readonly position: Span;
|
|
203
|
+
readonly value: string;
|
|
204
|
+
}
|
|
205
|
+
| {
|
|
206
|
+
readonly type: "code";
|
|
207
|
+
readonly position: Span;
|
|
208
|
+
readonly lang: string;
|
|
209
|
+
readonly value: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** An inline-config comment: an `html` node's text and where it sits. */
|
|
213
|
+
interface ConfigNode {
|
|
214
|
+
readonly value: string;
|
|
215
|
+
readonly position: Span;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `xs[i]` for an index this module computed from `xs` itself. The default is what arithmetic on a
|
|
220
|
+
* missing element gave in the JavaScript this was written in: `undefined` becomes NaN, not 0.
|
|
221
|
+
*/
|
|
222
|
+
const nth = (xs: readonly number[], i: number): number => {
|
|
223
|
+
const { [i]: v = Number.NaN } = xs;
|
|
224
|
+
return v;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const HEADING: Readonly<Record<string, number>> = {
|
|
228
|
+
part: 1,
|
|
229
|
+
section: 2,
|
|
230
|
+
subsection: 3,
|
|
231
|
+
subsubsection: 4,
|
|
232
|
+
};
|
|
111
233
|
const CODEISH =
|
|
112
234
|
/^(verbatim|lstlisting|minted|figure|figure\*|table|table\*|tabular|algorithm|algorithmic|thebibliography|tikzpicture|filecontents\*?)$/;
|
|
113
235
|
// Prose that a CODEISH environment has NO right to swallow together with the markup.
|
|
@@ -121,9 +243,24 @@ const FLOAT_PROSE = /^(caption|footnote)$/;
|
|
|
121
243
|
const OPAQUE =
|
|
122
244
|
/^(label|ref|autoref|eqref|cite|citep|citet|input|include|usepackage|documentclass|bibliography|bibliographystyle|acmISBN|acmDOI|acmConference|setcopyright|ccsdesc|keywords|orcid|affiliation|email|author|copyrightyear|acmYear|acmBooktitle|acmPrice|settopmatter|definecolor|includegraphics|newcommand|renewcommand|def|let|makeatletter|makeatother|hypersetup|pagestyle|thispagestyle|vspace|hspace)$/;
|
|
123
245
|
|
|
124
|
-
const P = (n) => n?.position;
|
|
246
|
+
const P = (n: TexNode | undefined): Span | undefined => n?.position;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Where the positioned nodes among `nodes` begin and end, or `undefined` when none is positioned.
|
|
250
|
+
* Nodes without a position are skipped rather than trusted, the way the projection always has.
|
|
251
|
+
*/
|
|
252
|
+
const spanOf = (
|
|
253
|
+
nodes: readonly TexNode[],
|
|
254
|
+
): { s: number; e: number } | undefined => {
|
|
255
|
+
const spans = nodes.map(P).filter((p): p is Span => p !== undefined);
|
|
256
|
+
const [first] = spans;
|
|
257
|
+
const last = spans.at(-1);
|
|
258
|
+
return first && last
|
|
259
|
+
? { s: first.start.offset, e: last.end.offset }
|
|
260
|
+
: undefined;
|
|
261
|
+
};
|
|
125
262
|
|
|
126
|
-
function plain(nodes) {
|
|
263
|
+
function plain(nodes: readonly TexNode[] | undefined): string {
|
|
127
264
|
let out = "";
|
|
128
265
|
for (const n of nodes || []) {
|
|
129
266
|
if (n.type === "string") out += n.content;
|
|
@@ -135,11 +272,16 @@ function plain(nodes) {
|
|
|
135
272
|
}
|
|
136
273
|
return out.trim();
|
|
137
274
|
}
|
|
138
|
-
const argEnd = (
|
|
139
|
-
|
|
275
|
+
const argEnd = (
|
|
276
|
+
node: { readonly args?: readonly TexArg[] },
|
|
277
|
+
start: Point,
|
|
278
|
+
): Point => {
|
|
279
|
+
let end = start;
|
|
140
280
|
for (const a of node.args || [])
|
|
141
|
-
for (const c of a.content || [])
|
|
142
|
-
|
|
281
|
+
for (const c of a.content || []) {
|
|
282
|
+
const at = P(c);
|
|
283
|
+
if (at && at.end.offset > end.offset) end = at.end;
|
|
284
|
+
}
|
|
143
285
|
return end;
|
|
144
286
|
};
|
|
145
287
|
|
|
@@ -148,28 +290,43 @@ const argEnd = (node) => {
|
|
|
148
290
|
* node shapes today's parser never emits (a macro without `args`, an argument without `content`, an
|
|
149
291
|
* `env` given as nodes), which the fallbacks below keep reading instead of throwing on.
|
|
150
292
|
*/
|
|
151
|
-
|
|
293
|
+
/** Calls `f` on each node in `content`: none for text or nothing, the node itself, or each of a list. */
|
|
294
|
+
function forEachNode(
|
|
295
|
+
content: TexNode | readonly TexNode[] | string | undefined,
|
|
296
|
+
f: (n: TexNode) => void,
|
|
297
|
+
): void {
|
|
298
|
+
if (!content || typeof content !== "object") return;
|
|
299
|
+
if ("type" in content) f(content);
|
|
300
|
+
else for (const n of content) forEachNode(n, f);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function texToMdast(
|
|
304
|
+
src: string,
|
|
305
|
+
{
|
|
306
|
+
parse = (s: string): TexRoot => getParser().parse(s),
|
|
307
|
+
}: { parse?: (s: string) => TexRoot } = {},
|
|
308
|
+
): { root: MdNode; text: string } {
|
|
152
309
|
const ast = parse(src);
|
|
153
310
|
const chars = src.split("");
|
|
154
|
-
const blank = (from, to) => {
|
|
311
|
+
const blank = (from: number, to: number) => {
|
|
155
312
|
for (let i = from; i < to && i < chars.length; i++)
|
|
156
313
|
if (chars[i] !== "\n") chars[i] = " ";
|
|
157
314
|
};
|
|
158
315
|
const lineStarts = [0];
|
|
159
316
|
for (let i = 0; i < src.length; i++)
|
|
160
317
|
if (src[i] === "\n") lineStarts.push(i + 1);
|
|
161
|
-
const loc = (off) => {
|
|
318
|
+
const loc = (off: number): Point => {
|
|
162
319
|
let lo = 0,
|
|
163
320
|
hi = lineStarts.length - 1;
|
|
164
321
|
while (lo < hi) {
|
|
165
322
|
const m = (lo + hi + 1) >> 1;
|
|
166
|
-
if (lineStarts
|
|
323
|
+
if (nth(lineStarts, m) <= off) lo = m;
|
|
167
324
|
else hi = m - 1;
|
|
168
325
|
}
|
|
169
|
-
return { line: lo + 1, column: off - lineStarts
|
|
326
|
+
return { line: lo + 1, column: off - nth(lineStarts, lo) + 1, offset: off };
|
|
170
327
|
};
|
|
171
|
-
const children = [];
|
|
172
|
-
const pendingStrong = [];
|
|
328
|
+
const children: MdNode[] = [];
|
|
329
|
+
const pendingStrong: number[] = [];
|
|
173
330
|
// Nesting depth inside a `\caption{}` / `\footnote{}` argument. Exactly one place needs it:
|
|
174
331
|
// bold lead-in synthesis. An `\emph{…}` in the first column INSIDE a caption is an editor's
|
|
175
332
|
// line break, not the heading of an appendix block, and a block-note rule would take it for
|
|
@@ -177,12 +334,19 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
177
334
|
// free half (0 findings), so this guards against tomorrow rather than repairing today.
|
|
178
335
|
let inCaption = 0;
|
|
179
336
|
/** A synthetic heading: the ATX marker is written at the START of the span, the tail is blanked. */
|
|
180
|
-
const synthHeading = (
|
|
337
|
+
const synthHeading = (
|
|
338
|
+
from: number,
|
|
339
|
+
to: number,
|
|
340
|
+
title: string,
|
|
341
|
+
depth = 2,
|
|
342
|
+
): MdNode | null => {
|
|
181
343
|
const label = "#".repeat(depth) + " " + title;
|
|
182
344
|
if (label.length > to - from) return null; // does not fit — do not invent one silently
|
|
183
345
|
blank(from, to);
|
|
184
|
-
|
|
185
|
-
|
|
346
|
+
label.split("").forEach((ch, i) => {
|
|
347
|
+
chars[from + i] = ch;
|
|
348
|
+
});
|
|
349
|
+
const node: MdNode = {
|
|
186
350
|
type: "heading",
|
|
187
351
|
depth,
|
|
188
352
|
title,
|
|
@@ -220,14 +384,11 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
220
384
|
// -- …`) was therefore invisible. Its `%` lines are made comments here, the only way
|
|
221
385
|
// `getInlineConfigNodes` sees them. BibTeX ignores text between entries, so the line is
|
|
222
386
|
// harmless to the build.
|
|
223
|
-
for (const n of ast.content)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
) {
|
|
229
|
-
let at = P(n).start.offset;
|
|
230
|
-
for (const line of src.slice(at, P(n).end.offset).split("\n")) {
|
|
387
|
+
for (const n of ast.content) {
|
|
388
|
+
const np = P(n);
|
|
389
|
+
if (n.type === "verbatim" && /^filecontents\*?$/.test(n.env) && np) {
|
|
390
|
+
let at = np.start.offset;
|
|
391
|
+
for (const line of src.slice(at, np.end.offset).split("\n")) {
|
|
231
392
|
const text = line.replace(/\r$/, "");
|
|
232
393
|
const pct = /^\s*%/.test(text) ? text.indexOf("%") : -1;
|
|
233
394
|
if (pct >= 0)
|
|
@@ -239,17 +400,22 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
239
400
|
at += line.length + 1;
|
|
240
401
|
}
|
|
241
402
|
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** What `walk` descends into: a node's `content` (nodes, or a string it skips) and `args`. */
|
|
406
|
+
const parts = (n: TexNode) => [
|
|
407
|
+
"content" in n ? n.content : undefined,
|
|
408
|
+
"args" in n ? n.args : undefined,
|
|
409
|
+
];
|
|
242
410
|
|
|
243
|
-
(function walk(node) {
|
|
244
|
-
if (Array.isArray(node)) return node.forEach((n) => walk(n));
|
|
245
|
-
if (!node || typeof node !== "object") return;
|
|
411
|
+
forEachNode(ast.content, function walk(node: TexNode): void {
|
|
246
412
|
const pos = P(node);
|
|
247
413
|
// The preamble is already blanked in full; nothing inside it (including a
|
|
248
414
|
// `\renewcommand{\bibliography}`) is a node of the document. Without this cut-off, one real
|
|
249
415
|
// paper got a FALSE `## References` on line 178 — i.e. its bibliography "began" before the
|
|
250
416
|
// introduction, and the whole paper fell into the free half.
|
|
251
417
|
if (pos && pos.start.offset < preEnd) {
|
|
252
|
-
for (const
|
|
418
|
+
for (const v of parts(node)) forEachNode(v, walk);
|
|
253
419
|
return;
|
|
254
420
|
}
|
|
255
421
|
|
|
@@ -265,7 +431,7 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
265
431
|
pos.start.offset + "\\begin{abstract}".length,
|
|
266
432
|
"Abstract",
|
|
267
433
|
);
|
|
268
|
-
for (const
|
|
434
|
+
for (const v of parts(node)) forEachNode(v, walk);
|
|
269
435
|
return;
|
|
270
436
|
}
|
|
271
437
|
if (env === "thebibliography" && pos) {
|
|
@@ -293,26 +459,17 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
293
459
|
// Removing `table` / `figure` from `CODEISH` was NOT an option: prose rules would then
|
|
294
460
|
// receive `&`, `\\` and the column specification, i.e. findings on markup — and for a
|
|
295
461
|
// rule somebody turns up to `error`, a false positive is worse than a miss.
|
|
296
|
-
const keep = [];
|
|
297
|
-
(function findProse(n) {
|
|
298
|
-
if (Array.isArray(n)) return n.forEach(findProse);
|
|
299
|
-
if (!n || typeof n !== "object") return;
|
|
462
|
+
const keep: { node: TexNode; s: number; e: number }[] = [];
|
|
463
|
+
forEachNode(node.content, function findProse(n: TexNode): void {
|
|
300
464
|
if (n.type === "macro" && FLOAT_PROSE.test(n.content)) {
|
|
301
|
-
const
|
|
302
|
-
.flatMap((a) => a.content || [])
|
|
303
|
-
.filter((c) => P(c));
|
|
465
|
+
const span = spanOf((n.args || []).flatMap((a) => a.content || []));
|
|
304
466
|
// Keep exactly the CONTENT of the argument: `\caption` itself and the braces
|
|
305
467
|
// stay in the blanked part, so the macro name never becomes prose.
|
|
306
|
-
if (
|
|
307
|
-
keep.push({
|
|
308
|
-
node: n,
|
|
309
|
-
s: P(cs[0]).start.offset,
|
|
310
|
-
e: P(cs[cs.length - 1]).end.offset,
|
|
311
|
-
});
|
|
468
|
+
if (span) keep.push({ node: n, ...span });
|
|
312
469
|
return; // do not look for a caption inside a caption
|
|
313
470
|
}
|
|
314
|
-
for (const
|
|
315
|
-
})
|
|
471
|
+
for (const v of parts(n)) forEachNode(v, findProse);
|
|
472
|
+
});
|
|
316
473
|
keep.sort((a, b) => a.s - b.s);
|
|
317
474
|
let cur = pos.start.offset;
|
|
318
475
|
for (const k of keep) {
|
|
@@ -324,7 +481,7 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
324
481
|
// document splitter drops from its clean text every line a `code` node TOUCHES, so one
|
|
325
482
|
// node spanning the whole float would carry the caption back into darkness right after
|
|
326
483
|
// the projection had saved it.
|
|
327
|
-
const proseLines = new Set();
|
|
484
|
+
const proseLines = new Set<number>();
|
|
328
485
|
for (const k of keep)
|
|
329
486
|
for (
|
|
330
487
|
let l = loc(k.s).line;
|
|
@@ -338,8 +495,9 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
338
495
|
if (proseLines.has(l)) continue;
|
|
339
496
|
let r = l;
|
|
340
497
|
while (r + 1 <= L1 && !proseLines.has(r + 1)) r++;
|
|
341
|
-
const s0 = lineStarts
|
|
342
|
-
const e0 =
|
|
498
|
+
const s0 = nth(lineStarts, l - 1);
|
|
499
|
+
const e0 =
|
|
500
|
+
r < lineStarts.length ? nth(lineStarts, r) - 1 : src.length;
|
|
343
501
|
children.push({
|
|
344
502
|
type: "code",
|
|
345
503
|
lang: env,
|
|
@@ -399,12 +557,14 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
399
557
|
const depth = HEADING[node.content];
|
|
400
558
|
if (depth) {
|
|
401
559
|
const title = plain(node.args?.flatMap((a) => a.content || []));
|
|
402
|
-
const end = argEnd(node);
|
|
560
|
+
const end = argEnd(node, pos.end);
|
|
403
561
|
// project to ATX: `##` where `\se` was, the rest spaces; the closing `}` too
|
|
404
562
|
blank(pos.start.offset, end.offset + 1);
|
|
405
563
|
for (let i = 0; i < depth; i++) chars[pos.start.offset + i] = "#";
|
|
406
564
|
const tStart = end.offset - title.length;
|
|
407
|
-
|
|
565
|
+
title.split("").forEach((ch, i) => {
|
|
566
|
+
chars[tStart + i] = ch;
|
|
567
|
+
});
|
|
408
568
|
const tPos = { start: loc(tStart), end: loc(end.offset) };
|
|
409
569
|
children.push({
|
|
410
570
|
type: "heading",
|
|
@@ -418,24 +578,26 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
418
578
|
});
|
|
419
579
|
return;
|
|
420
580
|
}
|
|
421
|
-
if (node.content === "bibliography"
|
|
422
|
-
synthHeading(
|
|
581
|
+
if (node.content === "bibliography") {
|
|
582
|
+
synthHeading(
|
|
583
|
+
pos.start.offset,
|
|
584
|
+
argEnd(node, pos.end).offset + 1,
|
|
585
|
+
"References",
|
|
586
|
+
);
|
|
423
587
|
return;
|
|
424
588
|
}
|
|
425
589
|
if (OPAQUE.test(node.content)) {
|
|
426
|
-
blank(pos.start.offset, argEnd(node).offset + 1);
|
|
590
|
+
blank(pos.start.offset, argEnd(node, pos.end).offset + 1);
|
|
427
591
|
return;
|
|
428
592
|
}
|
|
429
593
|
// `\texttt{X}` → `` `X` ``: exactly the same character count, so offsets do not move.
|
|
430
594
|
// This is the only markdown markup the projection can reproduce without shifting: `**`
|
|
431
595
|
// needs TWO characters after the content, and there is only `}`.
|
|
432
596
|
if (node.content === "texttt" || node.content === "lstinline") {
|
|
433
|
-
const e = argEnd(node);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (cs.length) {
|
|
438
|
-
const s0 = P(cs[0]).start.offset;
|
|
597
|
+
const e = argEnd(node, pos.end);
|
|
598
|
+
const span = spanOf((node.args || []).flatMap((a) => a.content || []));
|
|
599
|
+
if (span) {
|
|
600
|
+
const s0 = span.s;
|
|
439
601
|
blank(pos.start.offset, s0 - 1);
|
|
440
602
|
chars[s0 - 1] = "`";
|
|
441
603
|
chars[e.offset] = "`";
|
|
@@ -447,7 +609,7 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
447
609
|
pos.start.column === 1 &&
|
|
448
610
|
!inCaption
|
|
449
611
|
) {
|
|
450
|
-
const end = argEnd(node);
|
|
612
|
+
const end = argEnd(node, pos.end);
|
|
451
613
|
const sv = plain(node.args?.flatMap((a) => a.content || []));
|
|
452
614
|
children.push({
|
|
453
615
|
type: "strong",
|
|
@@ -474,28 +636,26 @@ export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
|
474
636
|
}
|
|
475
637
|
// an ordinary macro: blank the NAME (`\emph`) and the argument's BRACES; the content is prose
|
|
476
638
|
blank(pos.start.offset, pos.end.offset);
|
|
477
|
-
|
|
478
|
-
const o = pendingStrong.pop();
|
|
639
|
+
for (const o of pendingStrong.splice(0)) {
|
|
479
640
|
chars[o] = "*";
|
|
480
641
|
chars[o + 1] = "*";
|
|
481
642
|
}
|
|
482
643
|
for (const a of node.args || []) {
|
|
483
|
-
const
|
|
484
|
-
if (!
|
|
485
|
-
const s0 =
|
|
486
|
-
e0 = P(cs[cs.length - 1]).end.offset;
|
|
644
|
+
const span = spanOf(a.content || []);
|
|
645
|
+
if (!span) continue;
|
|
646
|
+
const { s: s0, e: e0 } = span;
|
|
487
647
|
if (a.openMark) blank(s0 - a.openMark.length, s0);
|
|
488
648
|
if (a.closeMark) blank(e0, e0 + a.closeMark.length);
|
|
489
649
|
}
|
|
490
650
|
if (FLOAT_PROSE.test(node.content)) {
|
|
491
651
|
inCaption++;
|
|
492
|
-
for (const
|
|
652
|
+
for (const v of parts(node)) forEachNode(v, walk);
|
|
493
653
|
inCaption--;
|
|
494
654
|
return;
|
|
495
655
|
}
|
|
496
656
|
}
|
|
497
|
-
for (const
|
|
498
|
-
})
|
|
657
|
+
for (const v of parts(node)) forEachNode(v, walk);
|
|
658
|
+
});
|
|
499
659
|
|
|
500
660
|
children.sort((a, b) => a.position.start.offset - b.position.start.offset);
|
|
501
661
|
return {
|
|
@@ -512,12 +672,24 @@ const commentParser = new ConfigCommentParser();
|
|
|
512
672
|
const directiveStart =
|
|
513
673
|
/^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
|
|
514
674
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
675
|
+
const DIRECTIVES: ReadonlyMap<string, DirectiveType> = new Map([
|
|
676
|
+
["eslint-disable", "disable"],
|
|
677
|
+
["eslint-enable", "enable"],
|
|
678
|
+
["eslint-disable-next-line", "disable-next-line"],
|
|
679
|
+
["eslint-disable-line", "disable-line"],
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
class TexSourceCode extends TextSourceCodeBase<{
|
|
683
|
+
LangOptions: LanguageOptions;
|
|
684
|
+
RootNode: MdNode;
|
|
685
|
+
SyntaxElementWithLoc: MdNode;
|
|
686
|
+
ConfigNode: ConfigNode;
|
|
687
|
+
}> {
|
|
688
|
+
#steps: VisitNodeStep[] | undefined;
|
|
689
|
+
#parents = new WeakMap<MdNode, MdNode | undefined>();
|
|
690
|
+
#comments: ConfigNode[] = [];
|
|
691
|
+
#inline: ConfigNode[] | undefined;
|
|
692
|
+
override ast: MdNode;
|
|
521
693
|
/**
|
|
522
694
|
* `raw` is the UNPROJECTED `.tex`, and it is here because the projection is lossy by design:
|
|
523
695
|
* it blanks macros, so a rule that must count `\S\ref` or `Fig.~\ref` sees spaces. Those
|
|
@@ -528,62 +700,60 @@ class TexSourceCode extends TextSourceCodeBase {
|
|
|
528
700
|
* so an offset computed on `raw` addresses the same byte of `text`. A rule may therefore
|
|
529
701
|
* scan `raw` and report with the offset it found.
|
|
530
702
|
*/
|
|
531
|
-
raw;
|
|
532
|
-
constructor({
|
|
703
|
+
raw: string;
|
|
704
|
+
constructor({
|
|
705
|
+
text,
|
|
706
|
+
ast,
|
|
707
|
+
raw,
|
|
708
|
+
}: {
|
|
709
|
+
text: string;
|
|
710
|
+
ast: MdNode;
|
|
711
|
+
raw?: string | undefined;
|
|
712
|
+
}) {
|
|
533
713
|
super({ ast, text, lineEndingPattern: /\r?\n/u });
|
|
534
714
|
this.ast = ast;
|
|
535
715
|
this.raw = raw ?? text;
|
|
536
716
|
this.traverse();
|
|
537
717
|
}
|
|
538
|
-
getParent(node) {
|
|
718
|
+
override getParent(node: MdNode): MdNode | undefined {
|
|
539
719
|
return this.#parents.get(node);
|
|
540
720
|
}
|
|
541
|
-
getInlineConfigNodes() {
|
|
721
|
+
getInlineConfigNodes(): ConfigNode[] {
|
|
542
722
|
if (!this.#inline)
|
|
543
723
|
this.#inline = this.#comments
|
|
544
724
|
.filter((c) => directiveStart.test(c.value))
|
|
545
725
|
.map((c) => ({ value: c.value.trim(), position: c.position }));
|
|
546
726
|
return this.#inline;
|
|
547
727
|
}
|
|
548
|
-
getDisableDirectives() {
|
|
549
|
-
const directives = [];
|
|
728
|
+
getDisableDirectives(): { problems: FileProblem[]; directives: Directive[] } {
|
|
729
|
+
const directives: Directive[] = [];
|
|
550
730
|
for (const comment of this.getInlineConfigNodes()) {
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
"eslint-enable",
|
|
558
|
-
"eslint-disable-next-line",
|
|
559
|
-
"eslint-disable-line",
|
|
560
|
-
].includes(label)
|
|
561
|
-
)
|
|
731
|
+
const parsed = commentParser.parseDirective(comment.value);
|
|
732
|
+
// `directiveStart` admitted this comment, and every text it admits parses as a directive.
|
|
733
|
+
assert.ok(parsed, `unparsable directive: ${comment.value}`);
|
|
734
|
+
const { label, value, justification } = parsed;
|
|
735
|
+
const type = DIRECTIVES.get(label);
|
|
736
|
+
if (type)
|
|
562
737
|
directives.push(
|
|
563
|
-
new Directive({
|
|
564
|
-
type: label.slice(7),
|
|
565
|
-
node: comment,
|
|
566
|
-
value,
|
|
567
|
-
justification,
|
|
568
|
-
}),
|
|
738
|
+
new Directive({ type, node: comment, value, justification }),
|
|
569
739
|
);
|
|
570
740
|
}
|
|
571
741
|
return { problems: [], directives };
|
|
572
742
|
}
|
|
573
|
-
applyInlineConfig() {
|
|
743
|
+
applyInlineConfig(): { configs: never[]; problems: FileProblem[] } {
|
|
574
744
|
return { configs: [], problems: [] };
|
|
575
745
|
}
|
|
576
|
-
traverse() {
|
|
746
|
+
override traverse(): ArrayIterator<VisitNodeStep> {
|
|
577
747
|
if (this.#steps) return this.#steps.values();
|
|
578
|
-
const steps = (this.#steps = []);
|
|
579
|
-
const visit = (node, parent) => {
|
|
748
|
+
const steps: VisitNodeStep[] = (this.#steps = []);
|
|
749
|
+
const visit = (node: MdNode, parent?: MdNode) => {
|
|
580
750
|
this.#parents.set(node, parent);
|
|
581
751
|
steps.push(
|
|
582
752
|
new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }),
|
|
583
753
|
);
|
|
584
754
|
if (node.type === "html")
|
|
585
755
|
this.#comments.push({ value: node.value, position: node.position });
|
|
586
|
-
for (const c of node.children
|
|
756
|
+
for (const c of "children" in node ? node.children : []) visit(c, node);
|
|
587
757
|
steps.push(
|
|
588
758
|
new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }),
|
|
589
759
|
);
|
|
@@ -593,30 +763,48 @@ class TexSourceCode extends TextSourceCodeBase {
|
|
|
593
763
|
}
|
|
594
764
|
}
|
|
595
765
|
|
|
596
|
-
export const texLanguage
|
|
766
|
+
export const texLanguage: Language<{
|
|
767
|
+
LangOptions: LanguageOptions;
|
|
768
|
+
Code: TexSourceCode;
|
|
769
|
+
RootNode: MdNode;
|
|
770
|
+
Node: MdNode;
|
|
771
|
+
}> = {
|
|
597
772
|
fileType: "text",
|
|
598
773
|
lineStart: 1,
|
|
599
774
|
columnStart: 1,
|
|
600
775
|
nodeTypeKey: "type",
|
|
601
776
|
defaultLanguageOptions: {},
|
|
602
777
|
validateLanguageOptions() {},
|
|
603
|
-
parse(file) {
|
|
778
|
+
parse(file: File): ParseResult<MdNode> {
|
|
604
779
|
try {
|
|
605
780
|
const raw = String(file.body);
|
|
606
781
|
const { root, text } = texToMdast(raw);
|
|
607
782
|
return { ok: true, ast: root, projected: text, raw };
|
|
608
783
|
} catch (ex) {
|
|
609
|
-
|
|
784
|
+
// What the parser throws is an Error: its own syntax error, or the stack exhausted. ESLint
|
|
785
|
+
// prints a parse failure at its `line` and `column`, and a failure of the whole parse has
|
|
786
|
+
// no position of its own, so it is placed at the start of the file.
|
|
787
|
+
assert.ok(ex instanceof Error);
|
|
788
|
+
return { ok: false, errors: [Object.assign(ex, { line: 1, column: 1 })] };
|
|
610
789
|
}
|
|
611
790
|
},
|
|
612
|
-
createSourceCode(
|
|
791
|
+
createSourceCode(
|
|
792
|
+
file: File,
|
|
793
|
+
parseResult: OkParseResult<MdNode>,
|
|
794
|
+
): TexSourceCode {
|
|
795
|
+
// What `parse` above returned, read back: ESLint hands a language's extra fields through
|
|
796
|
+
// untyped, so they are narrowed here rather than trusted.
|
|
797
|
+
const projected: unknown = parseResult["projected"];
|
|
798
|
+
const raw: unknown = parseResult["raw"];
|
|
799
|
+
assert.ok(typeof projected === "string");
|
|
613
800
|
// 🔴 LOAD-BEARING: the SourceCode receives the PROJECTION, not the source. Lengths and
|
|
614
801
|
// offsets are equal, so the position of a finding points into the real `.tex` while the
|
|
615
802
|
// rule sees prose.
|
|
616
803
|
return new TexSourceCode({
|
|
617
|
-
text:
|
|
804
|
+
text: projected,
|
|
618
805
|
ast: parseResult.ast,
|
|
619
|
-
|
|
806
|
+
// Absent from a parse result made by hand; the source code then reads the projection.
|
|
807
|
+
raw: typeof raw === "string" ? raw : undefined,
|
|
620
808
|
});
|
|
621
809
|
},
|
|
622
810
|
};
|
|
@@ -25,13 +25,13 @@ import { tmpdir } from "node:os";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { Linter } from "eslint";
|
|
27
27
|
import markdown from "@eslint/markdown";
|
|
28
|
-
import { createChecker } from "../lib/check.
|
|
28
|
+
import { createChecker } from "../lib/check.ts";
|
|
29
29
|
|
|
30
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const FIX = join(HERE, "..", "fixtures", "paper-research-question");
|
|
32
32
|
|
|
33
|
-
const { texLanguage } = await import(join(HERE, "latex-language.
|
|
34
|
-
const rq = (await import(join(HERE, "paper-research-question.
|
|
33
|
+
const { texLanguage } = await import(join(HERE, "latex-language.ts"));
|
|
34
|
+
const rq = (await import(join(HERE, "paper-research-question.ts"))).default;
|
|
35
35
|
|
|
36
36
|
const check = createChecker();
|
|
37
37
|
|