paperlint 3.1.0 → 3.1.2
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +21 -105
- package/CONTRIBUTING.md +23 -25
- package/README.md +11 -7
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +131 -12
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +14 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +30 -14
- package/skills/verify-citations/SKILL.md.spec.ts +29 -13
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +560 -0
- package/src/adapters/references/index.ts +203 -19
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +23 -12
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/ports/check-references.ts +16 -2
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +257 -10
- package/src/references.ts +93 -17
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -15,7 +15,6 @@ import { test } from "vitest";
|
|
|
15
15
|
import {
|
|
16
16
|
levenshtein,
|
|
17
17
|
titleSimilarity,
|
|
18
|
-
titlesMatch,
|
|
19
18
|
yearMatch,
|
|
20
19
|
titleRelation,
|
|
21
20
|
bestTitleRelation,
|
|
@@ -30,6 +29,9 @@ import {
|
|
|
30
29
|
normalizeDoi,
|
|
31
30
|
normalizeArxiv,
|
|
32
31
|
normalizeIdentifiers,
|
|
32
|
+
normalizeTitle,
|
|
33
|
+
titleIdentity,
|
|
34
|
+
isValidCveId,
|
|
33
35
|
} from "./verify-cites.mjs";
|
|
34
36
|
|
|
35
37
|
/** One vitest case per check, named by it. */
|
|
@@ -52,17 +54,17 @@ ok(
|
|
|
52
54
|
"case/normalize identical titles",
|
|
53
55
|
);
|
|
54
56
|
ok(
|
|
55
|
-
|
|
57
|
+
titleSimilarity(
|
|
56
58
|
"Deep Residual Learning for Image Recognition",
|
|
57
59
|
"Deep Residual Learning for Image Recognitionn",
|
|
58
|
-
),
|
|
60
|
+
) >= 0.7,
|
|
59
61
|
"one typo still ≥0.70",
|
|
60
62
|
);
|
|
61
63
|
ok(
|
|
62
|
-
|
|
64
|
+
titleSimilarity(
|
|
63
65
|
"Attention Is All You Need",
|
|
64
66
|
"A Survey of Reinforcement Learning",
|
|
65
|
-
),
|
|
67
|
+
) < 0.7,
|
|
66
68
|
"unrelated titles < 0.70",
|
|
67
69
|
);
|
|
68
70
|
ok(yearMatch(2020, 2021), "year ±1 ok");
|
|
@@ -733,3 +735,147 @@ console.log("\n[S4] .bib parser edge cases:");
|
|
|
733
735
|
"bibitem DOI trailing period not swallowed",
|
|
734
736
|
);
|
|
735
737
|
}
|
|
738
|
+
|
|
739
|
+
// ── Edges of the pure layer: the defensive arms, each with the input that reaches it ──────────
|
|
740
|
+
|
|
741
|
+
test("levenshtein, normalizeTitle and titleIdentity take a missing string as empty", () => {
|
|
742
|
+
assert.deepEqual(
|
|
743
|
+
[
|
|
744
|
+
levenshtein(null, "ab"),
|
|
745
|
+
levenshtein("abc", undefined),
|
|
746
|
+
levenshtein("", ""),
|
|
747
|
+
normalizeTitle(undefined),
|
|
748
|
+
titleIdentity(undefined),
|
|
749
|
+
],
|
|
750
|
+
[2, 3, 0, "", ""],
|
|
751
|
+
);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("normalizeDoi / normalizeArxiv pass a missing value through untouched", () => {
|
|
755
|
+
assert.deepEqual(
|
|
756
|
+
[
|
|
757
|
+
normalizeDoi(undefined),
|
|
758
|
+
normalizeDoi(""),
|
|
759
|
+
normalizeArxiv(null),
|
|
760
|
+
normalizeArxiv(""),
|
|
761
|
+
],
|
|
762
|
+
[undefined, "", null, ""],
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("titleRelation: an acronym of one letter is not an acronym; scripts are told apart", () => {
|
|
767
|
+
assert.deepEqual(
|
|
768
|
+
[
|
|
769
|
+
// "x" is one letter: it cannot spell the initials of anything.
|
|
770
|
+
titleRelation("x", "extensible markup language"),
|
|
771
|
+
titleRelation("Нейронные сети", "Neural networks"),
|
|
772
|
+
titleRelation("ニューラルネットワーク", "Neural networks"),
|
|
773
|
+
titleRelation("Νευρωνικά δίκτυα", "Neural networks"),
|
|
774
|
+
titleRelation("الشبكات العصبية", "Neural networks"),
|
|
775
|
+
titleRelation("!!!", "Neural networks"),
|
|
776
|
+
],
|
|
777
|
+
[
|
|
778
|
+
"different",
|
|
779
|
+
"incomparable",
|
|
780
|
+
"incomparable",
|
|
781
|
+
"incomparable",
|
|
782
|
+
"incomparable",
|
|
783
|
+
"incomparable",
|
|
784
|
+
],
|
|
785
|
+
);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("yearMatch reads a year out of a string and ignores one that is not a year", () => {
|
|
789
|
+
assert.deepEqual(
|
|
790
|
+
[
|
|
791
|
+
yearMatch("2017-06", 2018),
|
|
792
|
+
yearMatch("2017", 2020),
|
|
793
|
+
yearMatch("n.d.", 2020),
|
|
794
|
+
],
|
|
795
|
+
[true, false, true],
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("classifyResolver: a title search with no `records` field is a miss", () => {
|
|
800
|
+
assert.deepEqual(
|
|
801
|
+
classifyResolver(
|
|
802
|
+
{ title: "A Paper" },
|
|
803
|
+
{ db: "crossref", transport: "ok", query: "title" },
|
|
804
|
+
),
|
|
805
|
+
{ db: "crossref", status: "title_miss" },
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("checkNvd with no CVE on the citation says nothing; isValidCveId of nothing is false", () => {
|
|
810
|
+
assert.deepEqual(
|
|
811
|
+
[checkNvd({}, null), isValidCveId(undefined)],
|
|
812
|
+
[null, false],
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("reduceVerdict: an arXiv id missed only by a non-authoritative source is unresolvable", () => {
|
|
817
|
+
const r = reduceVerdict({ id: "a", arxiv: "1706.03762" }, [
|
|
818
|
+
{
|
|
819
|
+
db: "semantic_scholar",
|
|
820
|
+
status: "id_unmatched",
|
|
821
|
+
query: "arxiv",
|
|
822
|
+
authoritative: false,
|
|
823
|
+
},
|
|
824
|
+
]);
|
|
825
|
+
assert.deepEqual(r, {
|
|
826
|
+
id: "a",
|
|
827
|
+
verdict: "unresolvable",
|
|
828
|
+
reason:
|
|
829
|
+
"arXiv id 1706.03762 missed only by non-authoritative sources (the reachable arXiv API did not definitively reject it) — cannot assert fabrication → unresolvable",
|
|
830
|
+
matched_db: null,
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test("parseBib: @comment/@string/@preamble are skipped, a bare value is read, an empty one is absent", () => {
|
|
835
|
+
const bib = [
|
|
836
|
+
"@comment{c, not an entry}",
|
|
837
|
+
"@string{s, x = {y}}",
|
|
838
|
+
'@article{k1, title = "Quoted Title", year = 2017, eprint = {1706.03762}, archiveprefix = {arXiv}}',
|
|
839
|
+
"@misc{k2, title = {T}, year = ,}",
|
|
840
|
+
].join("\n");
|
|
841
|
+
assert.deepEqual(parseBib(bib), [
|
|
842
|
+
{ id: "k1", title: "Quoted Title", year: "2017", arxiv: "1706.03762" },
|
|
843
|
+
{ id: "k2", title: "T" },
|
|
844
|
+
]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("parseBib's \\bibitem fallback reads an arXiv id and an \\emph title", () => {
|
|
848
|
+
const text = [
|
|
849
|
+
"\\begin{thebibliography}{9}",
|
|
850
|
+
"\\bibitem{a} A. Author. \\emph{A Long Enough Title}. arXiv:1706.03762, 2017.",
|
|
851
|
+
"\\end{thebibliography}",
|
|
852
|
+
].join("\n");
|
|
853
|
+
assert.deepEqual(parseBib(text), [
|
|
854
|
+
{
|
|
855
|
+
id: "a",
|
|
856
|
+
arxiv: "1706.03762",
|
|
857
|
+
year: "2017",
|
|
858
|
+
title: "A Long Enough Title",
|
|
859
|
+
},
|
|
860
|
+
]);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("parseArxivFeed: an entry without a title is skipped, one without a date has no year", () => {
|
|
864
|
+
assert.deepEqual(
|
|
865
|
+
parseArxivFeed("<entry><id>x</id></entry><entry><title>T</title></entry>"),
|
|
866
|
+
[{ title: "T", year: null }],
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("a two-letter token ending in s is depluralised to one letter, which spells no acronym", () => {
|
|
871
|
+
assert.equal(
|
|
872
|
+
titleRelation("TS models", "totally separate models"),
|
|
873
|
+
"different",
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
test("parseArxivFeed keeps an entry titled Error when it has no <id> to prove it is the sentinel", () => {
|
|
878
|
+
assert.deepEqual(parseArxivFeed("<entry><title>Error</title></entry>"), [
|
|
879
|
+
{ title: "Error", year: null },
|
|
880
|
+
]);
|
|
881
|
+
});
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
import { test } from "vitest";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type BanalFailure,
|
|
9
|
+
describe,
|
|
10
|
+
describeLine,
|
|
11
|
+
PERL_MISSING,
|
|
12
|
+
} from "./failure.ts";
|
|
8
13
|
import { acceptProbe } from "./probe.ts";
|
|
9
14
|
|
|
10
15
|
test("describe: perl missing carries the fix", () => {
|
|
@@ -61,3 +66,67 @@ test("acceptProbe: one page and a body size is accepted; anything else is `probe
|
|
|
61
66
|
assert.equal(!r.ok && r.error.kind, "probe-rejected");
|
|
62
67
|
assert.equal(acceptProbe({ pages: [], bodyfontsize: 10 }).ok, false);
|
|
63
68
|
});
|
|
69
|
+
|
|
70
|
+
test("describe: every other kind is one exact sentence", () => {
|
|
71
|
+
const cases: readonly [BanalFailure, string][] = [
|
|
72
|
+
[
|
|
73
|
+
{ kind: "process-failed", status: 2, stderrHead: "boom" },
|
|
74
|
+
"banal failed (exit 2): boom",
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
{ kind: "signalled", signal: "SIGKILL", stderrHead: "" },
|
|
78
|
+
"banal failed (SIGKILL): no output",
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
{ kind: "signalled", signal: "SIGTERM", stderrHead: "late" },
|
|
82
|
+
"banal failed (SIGTERM): late",
|
|
83
|
+
],
|
|
84
|
+
[{ kind: "spawn-failed", message: "EACCES" }, "banal failed: EACCES"],
|
|
85
|
+
[
|
|
86
|
+
{ kind: "timed-out", afterMs: 500 },
|
|
87
|
+
"banal failed: no answer after 500 ms",
|
|
88
|
+
],
|
|
89
|
+
[{ kind: "no-json", head: "" }, "banal printed no JSON: nothing"],
|
|
90
|
+
[{ kind: "no-json", head: "Usage:" }, "banal printed no JSON: Usage:"],
|
|
91
|
+
[
|
|
92
|
+
{ kind: "banal-error", stderrHead: "bad xml" },
|
|
93
|
+
"banal could not read the banal input XML: bad xml",
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
kind: "unexpected-shape",
|
|
98
|
+
issues: ["pages missing", "bodyfontsize not a number"],
|
|
99
|
+
},
|
|
100
|
+
"banal printed JSON that is not a measurement: pages missing; bodyfontsize not a number",
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
{ kind: "probe-rejected", got: { pages: [] } },
|
|
104
|
+
'banal ran on a one-page probe but measured nothing: {"pages":[]}',
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
{ kind: "download-failed", url: "https://x/banal", detail: "HTTP 404" },
|
|
108
|
+
"could not download banal from https://x/banal: HTTP 404",
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
{ kind: "not-pinned", path: "/c/banal" },
|
|
112
|
+
"banal in /c/banal is not the pinned one (sha256 differs)",
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
{
|
|
116
|
+
kind: "does-not-run",
|
|
117
|
+
path: "/c/banal",
|
|
118
|
+
why: { kind: "timed-out", afterMs: 9 },
|
|
119
|
+
},
|
|
120
|
+
"banal in /c/banal does not run: banal failed: no answer after 9 ms",
|
|
121
|
+
],
|
|
122
|
+
];
|
|
123
|
+
for (const [failure, sentence] of cases)
|
|
124
|
+
assert.deepEqual(describe(failure), [sentence]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("describeLine: a sha256 mismatch joins its three lines", () => {
|
|
128
|
+
assert.equal(
|
|
129
|
+
describeLine({ kind: "sha-mismatch", url: "u", expected: "e", got: "g" }),
|
|
130
|
+
"banal from u does not have the pinned sha256 — refusing to install it; expected e; got g",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
@@ -103,6 +103,22 @@ test("installer: ensure reports the download, and Ready says where and what was
|
|
|
103
103
|
assert.equal(i.check().ok, true);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
test("installer with no source: the pin is downloaded, and bytes that are not the pin are refused", () => {
|
|
107
|
+
const download = fixedDownload(BODY);
|
|
108
|
+
const io = memoryPorts({
|
|
109
|
+
run: scriptedProcess(() => exitedWith("")),
|
|
110
|
+
download,
|
|
111
|
+
});
|
|
112
|
+
const r = banalInstaller(io, parseBanalSettings({}, dirs)).ensure(() => {});
|
|
113
|
+
assert.ok(!r.ok);
|
|
114
|
+
assert.match(
|
|
115
|
+
r.error[0],
|
|
116
|
+
/does not have the pinned sha256 — refusing to install it/,
|
|
117
|
+
);
|
|
118
|
+
assert.equal(download.urls.length, 1);
|
|
119
|
+
assert.notEqual(download.urls[0], source.url);
|
|
120
|
+
});
|
|
121
|
+
|
|
106
122
|
test("installer: a failure is lines of text, the first saying what happened", () => {
|
|
107
123
|
const io = memoryPorts({
|
|
108
124
|
run: scriptedProcess(() => ({ kind: "not-found", file: "perl" })),
|
|
@@ -60,6 +60,11 @@ const ROWS: readonly (readonly [
|
|
|
60
60
|
exited({ stdout: "Usage: banal" }),
|
|
61
61
|
{ kind: "no-json", head: "Usage: banal" },
|
|
62
62
|
],
|
|
63
|
+
[
|
|
64
|
+
"nothing on stdout: the head is stderr's first line",
|
|
65
|
+
exited({ stdout: "", stderr: "\nCan't locate XML/Parser.pm\n" }),
|
|
66
|
+
{ kind: "no-json", head: "Can't locate XML/Parser.pm" },
|
|
67
|
+
],
|
|
63
68
|
[
|
|
64
69
|
// Guards: banal's own failure object exits 0 — it must not read as zero pages.
|
|
65
70
|
'banal\'s `"error": true` with exit 0',
|
|
@@ -16,7 +16,8 @@ import { join } from "node:path";
|
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import { afterAll as after, test } from "vitest";
|
|
18
18
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
19
|
-
import { curlDownload } from "./download.io.ts";
|
|
19
|
+
import { curlDownload, curlFailure } from "./download.io.ts";
|
|
20
|
+
import type { ProcessExit } from "../../ports/process.ts";
|
|
20
21
|
import { spawnProcess } from "../node/index.ts";
|
|
21
22
|
|
|
22
23
|
const root = realpathSync(
|
|
@@ -55,3 +56,44 @@ test("🔴 Download: no curl on PATH is named as that, not as a failed URL", ()
|
|
|
55
56
|
// Guards: a missing curl is its own diagnosis — once, it read "spawnSync curl ENOENT".
|
|
56
57
|
assert.deepEqual(!r.ok && r.error, { detail: "curl is not installed" });
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
test("curlFailure: every way curl can end, in its own words", () => {
|
|
61
|
+
const cases: readonly [ProcessExit, string | null][] = [
|
|
62
|
+
[{ kind: "exited", status: 0, stdout: "", stderr: "" }, null],
|
|
63
|
+
[{ kind: "spawn-failed", message: "EACCES" }, "EACCES"],
|
|
64
|
+
[
|
|
65
|
+
{ kind: "timed-out", afterMs: 40_000, stdout: "", stderr: "" },
|
|
66
|
+
"no answer after 40000 ms",
|
|
67
|
+
],
|
|
68
|
+
[{ kind: "exited", status: 22, stdout: "", stderr: "" }, "curl exited 22"],
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
kind: "exited",
|
|
72
|
+
status: 22,
|
|
73
|
+
stdout: "",
|
|
74
|
+
stderr: "curl: (22) 404\nmore",
|
|
75
|
+
},
|
|
76
|
+
"curl: (22) 404",
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
{ kind: "signalled", signal: "SIGTERM", stdout: "", stderr: "" },
|
|
80
|
+
"SIGTERM",
|
|
81
|
+
],
|
|
82
|
+
];
|
|
83
|
+
for (const [exit, words] of cases) assert.equal(curlFailure(exit), words);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Download: curl exits 0 and writes nothing — a failure that says so, not empty bytes", () => {
|
|
87
|
+
const tmp = at("dl-silent");
|
|
88
|
+
mkdirSync(tmp, { recursive: true });
|
|
89
|
+
const d = curlDownload({
|
|
90
|
+
run: { run: () => ({ kind: "exited", status: 0, stdout: "", stderr: "" }) },
|
|
91
|
+
env: {},
|
|
92
|
+
tmpDir: tmp,
|
|
93
|
+
});
|
|
94
|
+
assert.deepEqual(d.fetch("https://example.test/banal", 1_000), {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: { detail: "curl wrote no file" },
|
|
97
|
+
});
|
|
98
|
+
assert.deepEqual(readdirSync(tmp), []);
|
|
99
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The in-memory test doubles keep the contracts of the ports they stand in for. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { test } from "vitest";
|
|
4
|
+
import { fixedDownload, memoryWorkspace } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
test("memoryWorkspace: a `within` that throws is recorded as `threw`, and the error goes on", () => {
|
|
7
|
+
const ws = memoryWorkspace();
|
|
8
|
+
assert.throws(
|
|
9
|
+
() =>
|
|
10
|
+
ws.within("x-", (s) => {
|
|
11
|
+
s.write("f", "body", "read");
|
|
12
|
+
throw new Error("inside");
|
|
13
|
+
}),
|
|
14
|
+
/inside/,
|
|
15
|
+
);
|
|
16
|
+
assert.deepEqual(ws.ended, ["threw"]);
|
|
17
|
+
assert.deepEqual(ws.written, [{ name: "f", content: "body", mode: "read" }]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("fixedDownload: bytes are handed back as they are, and every URL is recorded", () => {
|
|
21
|
+
const bytes = new Uint8Array([1, 2, 3]);
|
|
22
|
+
const d = fixedDownload(bytes);
|
|
23
|
+
assert.deepEqual(d.fetch("https://a", 1), { ok: true, value: bytes });
|
|
24
|
+
assert.deepEqual(d.urls, ["https://a"]);
|
|
25
|
+
});
|
|
@@ -11,7 +11,8 @@ import { dirname } from "node:path";
|
|
|
11
11
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
12
12
|
import type { Files } from "../../ports/files.ts";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** An error's errno code; anything thrown that is not an Error has none. */
|
|
15
|
+
export const errorCode = (e: unknown): unknown =>
|
|
15
16
|
e instanceof Error ? (e as NodeJS.ErrnoException).code : undefined;
|
|
16
17
|
|
|
17
18
|
export const nodeFiles: Files = {
|
|
@@ -22,7 +23,7 @@ export const nodeFiles: Files = {
|
|
|
22
23
|
try {
|
|
23
24
|
return readFileSync(p);
|
|
24
25
|
} catch (e) {
|
|
25
|
-
if (
|
|
26
|
+
if (errorCode(e) === "ENOENT") return null;
|
|
26
27
|
throw e;
|
|
27
28
|
}
|
|
28
29
|
},
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { afterAll as after, test } from "vitest";
|
|
7
7
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
8
|
-
import { nodeFiles } from "./files.io.ts";
|
|
8
|
+
import { errorCode, nodeFiles } from "./files.io.ts";
|
|
9
9
|
|
|
10
10
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
|
|
11
11
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
@@ -26,3 +26,15 @@ test("Files: writeAtomic creates the directory and leaves no .part behind", () =
|
|
|
26
26
|
);
|
|
27
27
|
assert.equal(existsSync(`${p}.part`), false);
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
test("Files: a read that fails for another reason than absence throws — a directory is not an empty file", () => {
|
|
31
|
+
assert.throws(() => nodeFiles.readBytes(at()), { code: "EISDIR" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("errorCode: an Error's errno code; a thrown non-Error has none", () => {
|
|
35
|
+
assert.equal(
|
|
36
|
+
errorCode(Object.assign(new Error("x"), { code: "ENOENT" })),
|
|
37
|
+
"ENOENT",
|
|
38
|
+
);
|
|
39
|
+
assert.equal(errorCode("ENOENT"), undefined);
|
|
40
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import { test } from "vitest";
|
|
8
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
8
9
|
import type { Command } from "../../ports/process.ts";
|
|
9
10
|
import { spawnProcess } from "./process.io.ts";
|
|
10
11
|
|
|
@@ -44,3 +45,40 @@ test("the environment is the Command's, whole: nothing leaks in from the parent"
|
|
|
44
45
|
// Guards: `Command.env` is the child's entire environment — the adapter merges nothing.
|
|
45
46
|
assert.deepEqual(r.kind === "exited" && r.stdout, "[][1]\n");
|
|
46
47
|
});
|
|
48
|
+
|
|
49
|
+
test("a program killed by a signal is `signalled`, with what it printed first", () => {
|
|
50
|
+
assert.deepEqual(run.run(sh("echo before; kill -TERM $$")), {
|
|
51
|
+
kind: "signalled",
|
|
52
|
+
signal: "SIGTERM",
|
|
53
|
+
stdout: "before\n",
|
|
54
|
+
stderr: "",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("output past maxOutputBytes is a spawn failure, named by Node's own message", () => {
|
|
59
|
+
const r = run.run({ ...sh("head -c 100000 /dev/zero"), maxOutputBytes: 10 });
|
|
60
|
+
assert.equal(r.kind, "spawn-failed");
|
|
61
|
+
assert.match(r.kind === "spawn-failed" ? r.message : "", /ENOBUFS/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("cwd is the Command's when it names one", () => {
|
|
65
|
+
const r = run.run({ ...sh("pwd"), cwd: "/" as AbsolutePath });
|
|
66
|
+
assert.deepEqual(r.kind === "exited" && r.stdout, "/\n");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a spawn that reports no status, no signal and no error reads as exit 1, not success", () => {
|
|
70
|
+
const silent = spawnProcess((() => ({
|
|
71
|
+
pid: 0,
|
|
72
|
+
output: [],
|
|
73
|
+
stdout: "",
|
|
74
|
+
stderr: "",
|
|
75
|
+
status: null,
|
|
76
|
+
signal: null,
|
|
77
|
+
})) as never);
|
|
78
|
+
assert.deepEqual(silent.run(sh("true")), {
|
|
79
|
+
kind: "exited",
|
|
80
|
+
status: 1,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: "",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -109,3 +109,37 @@ check(
|
|
|
109
109
|
"fillsOf: an empty item is `unknown`",
|
|
110
110
|
L.fillsOf(OPS, ops(), [" "])[0]?.kind === "unknown",
|
|
111
111
|
);
|
|
112
|
+
|
|
113
|
+
// ── argument shapes pdf.js versions differ on ────────────────────────────────────────────
|
|
114
|
+
test("isUpright on a short transform; hexOf on a non-array and on non-numbers", () => {
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
[L.isUpright([]), L.hexOf(null), L.hexOf([255.4, "x", undefined])],
|
|
117
|
+
[true, "#000000", "#ff0000"],
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("the walk reads odd show-text arguments as no characters, and odd state operators as defaults", () => {
|
|
122
|
+
const drawn = L.drawnCharacters(
|
|
123
|
+
OPS,
|
|
124
|
+
ops(
|
|
125
|
+
[OPS.restore], // a restore with nothing saved: the default state
|
|
126
|
+
[OPS.setTextRenderingMode], // a mode with no argument: 0
|
|
127
|
+
[99], // an operator the walk does not read
|
|
128
|
+
[OPS.showText, "not an array"],
|
|
129
|
+
[OPS.showText, ["not glyphs"]],
|
|
130
|
+
[OPS.showText, [[7, null, { unicode: undefined }, { unicode: "A" }]]],
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
assert.deepEqual(drawn, [{ ch: "A", fill: { kind: "unknown" } }]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an item that matches only up to the end of the drawn text is not found", () => {
|
|
137
|
+
const list = ops(
|
|
138
|
+
[OPS.setFillRGBColor, ["#112233"]],
|
|
139
|
+
[OPS.showText, glyphs("ab")],
|
|
140
|
+
);
|
|
141
|
+
assert.deepEqual(L.fillsOf(OPS, list, ["bc", "b"]), [
|
|
142
|
+
{ kind: "unknown" },
|
|
143
|
+
{ kind: "rgb", hex: "#112233" },
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
@@ -81,40 +81,45 @@ function applyState(
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** One drawn character and the fill it was drawn in. */
|
|
85
|
+
interface Drawn {
|
|
86
|
+
readonly ch: string;
|
|
87
|
+
readonly fill: Fill;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
/** Every drawn character, in content order, with the fill it was drawn in. */
|
|
85
|
-
export function drawnCharacters(
|
|
86
|
-
ops: ColourOps,
|
|
87
|
-
list: OperatorList,
|
|
88
|
-
): { chars: string[]; fills: Fill[] } {
|
|
91
|
+
export function drawnCharacters(ops: ColourOps, list: OperatorList): Drawn[] {
|
|
89
92
|
const s = { cur: { fill: { kind: "unknown" } as Fill, mode: 0 }, stack: [] };
|
|
90
|
-
const
|
|
91
|
-
const fills: Fill[] = [];
|
|
93
|
+
const drawn: Drawn[] = [];
|
|
92
94
|
list.fnArray.forEach((fn, i) => {
|
|
93
95
|
const args = list.argsArray[i];
|
|
94
96
|
if (applyState(ops, fn, args, s)) return;
|
|
95
97
|
if (fn !== ops.showText && fn !== ops.showSpacedText) return;
|
|
96
98
|
const fill: Fill = s.cur.mode === 3 ? { kind: "invisible" } : s.cur.fill;
|
|
97
|
-
for (const ch of squeeze(glyphText(args))) {
|
|
98
|
-
chars.push(ch);
|
|
99
|
-
fills.push(fill);
|
|
100
|
-
}
|
|
99
|
+
for (const ch of squeeze(glyphText(args))) drawn.push({ ch, fill });
|
|
101
100
|
});
|
|
102
|
-
return
|
|
101
|
+
return drawn;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
/** How far ahead the walk looks for an item it cannot find where it expects it. */
|
|
106
105
|
export const LOOKAHEAD = 4000;
|
|
107
106
|
|
|
108
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Where `want` starts in `drawn`, at or after `from` within the look-ahead, and the fill it was
|
|
109
|
+
* drawn in — or null. The fill comes from the matched element itself, not a second lookup.
|
|
110
|
+
*/
|
|
109
111
|
function findRun(
|
|
110
|
-
|
|
112
|
+
drawn: readonly Drawn[],
|
|
111
113
|
want: readonly string[],
|
|
112
114
|
from: number,
|
|
113
|
-
): number {
|
|
114
|
-
const
|
|
115
|
-
for (
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
): { readonly at: number; readonly fill: Fill } | null {
|
|
116
|
+
const window = drawn.slice(from, from + LOOKAHEAD);
|
|
117
|
+
for (const [i, d] of window.entries()) {
|
|
118
|
+
const at = from + i;
|
|
119
|
+
if (want.every((ch, k) => drawn[at + k]?.ch === ch))
|
|
120
|
+
return { at, fill: d.fill };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/**
|
|
@@ -128,14 +133,28 @@ export function fillsOf(
|
|
|
128
133
|
list: OperatorList,
|
|
129
134
|
items: readonly string[],
|
|
130
135
|
): Fill[] {
|
|
131
|
-
|
|
136
|
+
return fillsFor(ops, list, items, (text) => text).map((p) => p.fill);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Each item paired with its fill — `fillsOf` for items that are not bare strings, so a caller never
|
|
141
|
+
* matches the two lists up by index.
|
|
142
|
+
*/
|
|
143
|
+
export function fillsFor<T>(
|
|
144
|
+
ops: ColourOps,
|
|
145
|
+
list: OperatorList,
|
|
146
|
+
items: readonly T[],
|
|
147
|
+
textOf: (item: T) => string,
|
|
148
|
+
): { readonly item: T; readonly fill: Fill }[] {
|
|
149
|
+
const drawn = drawnCharacters(ops, list);
|
|
132
150
|
let at = 0;
|
|
133
|
-
|
|
151
|
+
const fillOf = (text: string): Fill => {
|
|
134
152
|
const want = [...squeeze(text)];
|
|
135
153
|
if (want.length === 0) return { kind: "unknown" };
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
at =
|
|
139
|
-
return
|
|
140
|
-
}
|
|
154
|
+
const hit = findRun(drawn, want, at);
|
|
155
|
+
if (hit === null) return { kind: "unknown" };
|
|
156
|
+
at = hit.at + want.length;
|
|
157
|
+
return hit.fill;
|
|
158
|
+
};
|
|
159
|
+
return items.map((item) => ({ item, fill: fillOf(textOf(item)) }));
|
|
141
160
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The clock, for the lookup cache: the day an answer was fetched, in UTC. */
|
|
2
|
+
export const todayUtc = (): string => new Date().toISOString().slice(0, 10);
|
|
3
|
+
|
|
4
|
+
/** Wait `ms` — DBLP's pace between requests. */
|
|
5
|
+
export const sleep = (ms: number): Promise<void> =>
|
|
6
|
+
new Promise((r) => {
|
|
7
|
+
setTimeout(r, ms);
|
|
8
|
+
});
|