paperlint 10.4.1 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deadlines.yml +27 -7
- package/README.md +16 -15
- package/dist/adapters/hotcrp/index.d.ts +6 -4
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +6 -4
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/adapters/hotcrp/portal.io.d.ts +3 -1
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -1
- package/dist/adapters/hotcrp/portal.io.js +36 -11
- package/dist/adapters/hotcrp/portal.io.js.map +1 -1
- package/dist/adapters/hotcrp/response.d.ts +6 -1
- package/dist/adapters/hotcrp/response.d.ts.map +1 -1
- package/dist/adapters/hotcrp/response.js +17 -0
- package/dist/adapters/hotcrp/response.js.map +1 -1
- package/dist/adapters/hotcrp/site.d.ts +9 -0
- package/dist/adapters/hotcrp/site.d.ts.map +1 -0
- package/dist/adapters/hotcrp/site.js +22 -0
- package/dist/adapters/hotcrp/site.js.map +1 -0
- package/dist/adapters/hotcrp/token.d.ts +13 -0
- package/dist/adapters/hotcrp/token.d.ts.map +1 -0
- package/dist/adapters/hotcrp/token.js +48 -0
- package/dist/adapters/hotcrp/token.js.map +1 -0
- package/dist/adapters/latex/abstract.d.ts +22 -0
- package/dist/adapters/latex/abstract.d.ts.map +1 -0
- package/dist/adapters/latex/abstract.js +187 -0
- package/dist/adapters/latex/abstract.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +6 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -1
- package/dist/adapters/latex/headings.js +5 -4
- package/dist/adapters/latex/headings.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +15 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +58 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -8
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts.map +1 -1
- package/dist/cycle-rules.js +3 -4
- package/dist/cycle-rules.js.map +1 -1
- package/dist/domain/deadline.d.ts +8 -1
- package/dist/domain/deadline.d.ts.map +1 -1
- package/dist/domain/deadline.js +26 -12
- package/dist/domain/deadline.js.map +1 -1
- package/dist/domain/portal-token.d.ts +44 -0
- package/dist/domain/portal-token.d.ts.map +1 -0
- package/dist/domain/portal-token.js +32 -0
- package/dist/domain/portal-token.js.map +1 -0
- package/dist/domain/submission.d.ts +30 -0
- package/dist/domain/submission.d.ts.map +1 -1
- package/dist/domain/submission.js +68 -0
- package/dist/domain/submission.js.map +1 -1
- package/dist/lib/paper-config.d.ts +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +6 -4
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -1
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +11 -5
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +23 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/submission-portal.d.ts +39 -2
- package/dist/ports/submission-portal.d.ts.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +1 -1
- package/dist/submission.d.ts +49 -11
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +332 -44
- package/dist/submission.js.map +1 -1
- package/docs/configuration.md +2 -1
- package/docs/rules/cycle/record.md +9 -6
- package/docs/submission.md +47 -48
- package/eslint.config.mjs +3 -0
- package/lib/paper-config.ts +7 -4
- package/package.json +6 -1
- package/presets/msr.jsonc +9 -7
- package/presets/msr.md +14 -10
- package/scripts/check.ts +1 -1
- package/scripts/refresh-deadlines/compare.test.ts +75 -0
- package/scripts/refresh-deadlines/compare.ts +101 -0
- package/scripts/refresh-deadlines/main.test.ts +140 -0
- package/scripts/refresh-deadlines/main.ts +56 -0
- package/scripts/refresh-deadlines/refresh.test.ts +287 -0
- package/scripts/refresh-deadlines/refresh.ts +183 -0
- package/skills/submission-portal/SKILL.md +60 -44
- package/skills/submission-portal/SKILL.md.spec.ts +62 -43
- package/skills/submission-portal/submission-portal.test.ts +4 -73
- package/src/adapters/hotcrp/cassette/README.md +4 -4
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +9 -9
- package/src/adapters/hotcrp/deadlines.io.test.ts +2 -2
- package/src/adapters/hotcrp/deadlines.test.ts +3 -3
- package/src/adapters/hotcrp/index.ts +15 -3
- package/src/adapters/hotcrp/portal.io.ts +61 -17
- package/src/adapters/hotcrp/portal.test.ts +105 -1
- package/src/adapters/hotcrp/response.ts +21 -0
- package/src/adapters/hotcrp/site.ts +34 -0
- package/src/adapters/hotcrp/token.test.ts +69 -0
- package/src/adapters/hotcrp/token.ts +51 -0
- package/src/adapters/latex/abstract.test.ts +280 -0
- package/src/adapters/latex/abstract.ts +272 -0
- package/src/adapters/latex/headings.ts +5 -4
- package/src/adapters/latex/index.test.ts +31 -1
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/parse.ts +64 -3
- package/src/cli.test.ts +20 -8
- package/src/cli.ts +22 -13
- package/src/cycle-rules.test.ts +6 -6
- package/src/cycle-rules.ts +7 -4
- package/src/domain/deadline.test.ts +31 -2
- package/src/domain/deadline.ts +42 -16
- package/src/domain/portal-token.test.ts +55 -0
- package/src/domain/portal-token.ts +63 -0
- package/src/domain/submission.test.ts +88 -0
- package/src/domain/submission.ts +99 -0
- package/src/new-paper.test.ts +1 -0
- package/src/paper-settings.test.ts +20 -0
- package/src/paper-settings.ts +17 -5
- package/src/ports/latex.ts +25 -0
- package/src/ports/submission-portal.ts +43 -1
- package/src/ports/venue-deadlines.ts +1 -1
- package/src/presets.test.ts +8 -5
- package/src/submission-new.test.ts +569 -0
- package/src/submission-show.test.ts +210 -0
- package/src/submission-update.test.ts +160 -0
- package/src/submission.test.ts +140 -518
- package/src/submission.ts +494 -61
- package/scripts/refresh-deadlines.test.ts +0 -431
- package/scripts/refresh-deadlines.ts +0 -387
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HotCRP site before any token, as the `PortalSite` port: the variable its token is read from,
|
|
3
|
+
* where on the site an author makes one and with which scopes, what `submission new` leaves to the
|
|
4
|
+
* site, a submission's page — and the API, once there is a token. Every word here is HotCRP's.
|
|
5
|
+
*/
|
|
6
|
+
import type { PortalSite } from "../../ports/submission-portal.ts";
|
|
7
|
+
import { hotcrpPortal } from "./portal.io.ts";
|
|
8
|
+
import {
|
|
9
|
+
SITE_LESS_VARIABLE,
|
|
10
|
+
VARIABLE_PATTERN,
|
|
11
|
+
tokenVariable,
|
|
12
|
+
} from "./token.ts";
|
|
13
|
+
|
|
14
|
+
/** The HotCRP site at `url` (no trailing slash). */
|
|
15
|
+
export const hotcrpSite = (url: string): PortalSite => ({
|
|
16
|
+
tokenVariable: tokenVariable(url),
|
|
17
|
+
siteLessVariable: SITE_LESS_VARIABLE,
|
|
18
|
+
variablePattern: VARIABLE_PATTERN,
|
|
19
|
+
credentials: {
|
|
20
|
+
// HotCRP's own link to it: `hotlink("Profile > Developer", "profile", ["t" => "developer"])`
|
|
21
|
+
// (src/help/h_developer.php in kohler/hotcrp).
|
|
22
|
+
page: `${url}/profile?t=developer`,
|
|
23
|
+
where: "Account settings → Developer → API tokens",
|
|
24
|
+
scopes:
|
|
25
|
+
"submeta:read and document:read (show), submeta:write and document:write (update, new) — tick all four to use every subcommand",
|
|
26
|
+
},
|
|
27
|
+
siteOnly: [
|
|
28
|
+
"PC conflicts (HotCRP's API takes them as pc_conflicts; paperlint does not send them)",
|
|
29
|
+
"topics, and every field the venue adds (paper type, artifact, consent boxes …)",
|
|
30
|
+
],
|
|
31
|
+
submissionPage: (id) =>
|
|
32
|
+
`${url}/paper/${id === null ? "<its number>" : String(id)}`,
|
|
33
|
+
connect: (token) => hotcrpPortal({ url, token }),
|
|
34
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HotCRP site's token variable — one per site, never shared by two: on the hosted service
|
|
3
|
+
* (`*.hotcrp.com`) the subdomain, anywhere else the whole host and path.
|
|
4
|
+
*/
|
|
5
|
+
import { map, unique } from "remeda";
|
|
6
|
+
import { expect, test } from "vitest";
|
|
7
|
+
import { tokenVariable } from "./index.ts";
|
|
8
|
+
|
|
9
|
+
test("tokenVariable: on hotcrp.com the subdomain alone names the site", () => {
|
|
10
|
+
expect(tokenVariable("https://msr2027.hotcrp.com")).toBe(
|
|
11
|
+
"HOTCRP_TOKEN_MSR2027",
|
|
12
|
+
);
|
|
13
|
+
expect(tokenVariable("https://msr2027.hotcrp.com/")).toBe(
|
|
14
|
+
"HOTCRP_TOKEN_MSR2027",
|
|
15
|
+
);
|
|
16
|
+
expect(tokenVariable("https://ase-2027.hotcrp.com")).toBe(
|
|
17
|
+
"HOTCRP_TOKEN_ASE_2027",
|
|
18
|
+
);
|
|
19
|
+
expect(tokenVariable("msr2027.hotcrp.com")).toBe("HOTCRP_TOKEN_MSR2027");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("tokenVariable: any other host — the whole host and path, upper-cased, non-alphanumerics as `_`", () => {
|
|
23
|
+
expect(tokenVariable("https://aidc.submit.acsac.org")).toBe(
|
|
24
|
+
"HOTCRP_TOKEN_AIDC_SUBMIT_ACSAC_ORG",
|
|
25
|
+
);
|
|
26
|
+
expect(tokenVariable("https://hotcrp.example.edu/conf2027")).toBe(
|
|
27
|
+
"HOTCRP_TOKEN_HOTCRP_EXAMPLE_EDU_CONF2027",
|
|
28
|
+
);
|
|
29
|
+
expect(tokenVariable("https://hotcrp.example.edu/conf2027/")).toBe(
|
|
30
|
+
"HOTCRP_TOKEN_HOTCRP_EXAMPLE_EDU_CONF2027",
|
|
31
|
+
);
|
|
32
|
+
expect(tokenVariable("hotcrp.example.edu")).toBe(
|
|
33
|
+
"HOTCRP_TOKEN_HOTCRP_EXAMPLE_EDU",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("tokenVariable: two sites never share a variable — same first label, same last path segment", () => {
|
|
38
|
+
const sites = [
|
|
39
|
+
"https://conf.hotcrp.com",
|
|
40
|
+
"https://conf.example.edu",
|
|
41
|
+
"https://a.example.edu/conf2027",
|
|
42
|
+
"https://b.example.edu/conf2027",
|
|
43
|
+
"https://hotcrp.example.edu/conf2027",
|
|
44
|
+
"https://hotcrp.example.edu/conf2028",
|
|
45
|
+
];
|
|
46
|
+
const names = map(sites, tokenVariable);
|
|
47
|
+
expect(unique(names)).toStrictEqual(names);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("tokenVariable: a port other than the scheme's default is part of the site; the default changes nothing", () => {
|
|
51
|
+
expect(
|
|
52
|
+
map(
|
|
53
|
+
[
|
|
54
|
+
"https://review.example.org:8443/conf",
|
|
55
|
+
"https://review.example.org:9443/conf",
|
|
56
|
+
"https://review.example.org:443/conf",
|
|
57
|
+
"https://review.example.org/conf",
|
|
58
|
+
"http://127.0.0.1:9",
|
|
59
|
+
],
|
|
60
|
+
tokenVariable,
|
|
61
|
+
),
|
|
62
|
+
).toStrictEqual([
|
|
63
|
+
"HOTCRP_TOKEN_REVIEW_EXAMPLE_ORG_8443_CONF",
|
|
64
|
+
"HOTCRP_TOKEN_REVIEW_EXAMPLE_ORG_9443_CONF",
|
|
65
|
+
"HOTCRP_TOKEN_REVIEW_EXAMPLE_ORG_CONF",
|
|
66
|
+
"HOTCRP_TOKEN_REVIEW_EXAMPLE_ORG_CONF",
|
|
67
|
+
"HOTCRP_TOKEN_127_0_0_1_9",
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH ENVIRONMENT VARIABLE HOLDS A HOTCRP SITE'S API TOKEN — HotCRP's fact, so HotCRP's adapter.
|
|
3
|
+
* Pure.
|
|
4
|
+
*
|
|
5
|
+
* A HotCRP token works on the site it was made on and nowhere else (measured 2026-10-19: a token
|
|
6
|
+
* made on one HotCRP site gets `401 Unauthorized` from `msr2027.hotcrp.com`'s `/api/whoami`). So the
|
|
7
|
+
* variable is named after the site, and no two sites share one — `HOTCRP_TOKEN_<subdomain>` on the
|
|
8
|
+
* hosted service, `HOTCRP_TOKEN_<host and path>` anywhere else — and an author with live submissions
|
|
9
|
+
* at two venues sets two variables (#236). The site-less `HOTCRP_TOKEN` is no longer read; the policy that refuses it
|
|
10
|
+
* by name is the domain's (`src/domain/portal-token.ts`).
|
|
11
|
+
*/
|
|
12
|
+
import { hostOf } from "../../domain/portal-token.ts";
|
|
13
|
+
|
|
14
|
+
/** The site-less variable, read until #236: named when it is still set, never used. */
|
|
15
|
+
export const SITE_LESS_VARIABLE = "HOTCRP_TOKEN";
|
|
16
|
+
|
|
17
|
+
/** The shape of a site's variable, as a message names it. */
|
|
18
|
+
export const VARIABLE_PATTERN = `${SITE_LESS_VARIABLE}_<LABEL>`;
|
|
19
|
+
|
|
20
|
+
/** The hosted service, where each conference is a subdomain of its own. */
|
|
21
|
+
const HOSTED = ".hotcrp.com";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What names a site, and no other: on the hosted service the subdomain alone
|
|
25
|
+
* (`msr2027.hotcrp.com` → `msr2027`); anywhere else the whole host, with a port other than the
|
|
26
|
+
* scheme's default, and path (`hotcrp.example.edu/conf2027`, `review.example.org:8443/conf`),
|
|
27
|
+
* because one install may serve several conferences under paths or ports, and two hosts may share a
|
|
28
|
+
* first label.
|
|
29
|
+
*/
|
|
30
|
+
function siteLabel(url: string): string {
|
|
31
|
+
const host = hostOf(url);
|
|
32
|
+
if (host.endsWith(HOSTED)) return host.slice(0, -HOSTED.length);
|
|
33
|
+
const path = URL.canParse(url) ? new URL(url).pathname : "";
|
|
34
|
+
return `${host}${path}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The variable a HotCRP site's token is read from: `HOTCRP_TOKEN_` and the site's label upper-cased,
|
|
39
|
+
* every run of characters but letters and digits made one `_`, none at either end.
|
|
40
|
+
* `https://msr2027.hotcrp.com` → `HOTCRP_TOKEN_MSR2027`; `https://aidc.submit.acsac.org` →
|
|
41
|
+
* `HOTCRP_TOKEN_AIDC_SUBMIT_ACSAC_ORG`; `https://hotcrp.example.edu/conf2027` →
|
|
42
|
+
* `HOTCRP_TOKEN_HOTCRP_EXAMPLE_EDU_CONF2027`.
|
|
43
|
+
*/
|
|
44
|
+
export function tokenVariable(url: string): string {
|
|
45
|
+
const label = siteLabel(url);
|
|
46
|
+
const name = label
|
|
47
|
+
.toUpperCase()
|
|
48
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
49
|
+
.replace(/^_+|_+$/g, "");
|
|
50
|
+
return `${SITE_LESS_VARIABLE}_${name}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abstract as plain text, from `\begin{abstract}…\end{abstract}` in the paper's source: the
|
|
3
|
+
* author's argument-less macros expanded, TeX's ligatures for dashes and quotes, citations and
|
|
4
|
+
* comments dropped — and anything else refused by name and place, never guessed.
|
|
5
|
+
*/
|
|
6
|
+
import { map } from "remeda";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import type * as Ast from "@unified-latex/unified-latex-types";
|
|
9
|
+
import { abstractOfTree } from "./abstract.ts";
|
|
10
|
+
import { latexReader } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
const doc = (preamble: string, abstract: string): string =>
|
|
13
|
+
`\\documentclass{article}\n${preamble}\n\\begin{document}\n\\begin{abstract}\n${abstract}\n\\end{abstract}\n\\end{document}\n`;
|
|
14
|
+
|
|
15
|
+
const text = (preamble: string, abstract: string) =>
|
|
16
|
+
latexReader.abstractText(doc(preamble, abstract));
|
|
17
|
+
|
|
18
|
+
describe("abstractText — what TeX prints, as plain text", () => {
|
|
19
|
+
it("words, spaces and paragraphs collapse to one paragraph", () => {
|
|
20
|
+
expect(text("", "One line,\nand\n\nanother.")).toEqual({
|
|
21
|
+
ok: true,
|
|
22
|
+
value: "One line, and another.",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("argument-less macros of the author's own are expanded — \\newcommand, \\renewcommand, \\def — nested too", () => {
|
|
27
|
+
expect(
|
|
28
|
+
text(
|
|
29
|
+
"\\newcommand{\\numX}{83}\\renewcommand\\tool{GateBench}\\def\\pct{\\numX\\%}",
|
|
30
|
+
"\\tool{} blocked \\numX{} attacks (\\pct) and \\tool\\ held.",
|
|
31
|
+
),
|
|
32
|
+
).toEqual({
|
|
33
|
+
ok: true,
|
|
34
|
+
value: "GateBench blocked 83 attacks (83%) and GateBench held.",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("TeX's ligatures: -- and --- as dashes, `` '' ` ' as curly quotes, ~ as a space", () => {
|
|
39
|
+
expect(
|
|
40
|
+
text("", "pages 1--2---done; ``quoted'' and `single', it's~here."),
|
|
41
|
+
).toEqual({
|
|
42
|
+
ok: true,
|
|
43
|
+
value: "pages 1–2—done; “quoted” and ‘single’, it’s here.",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("citations and comments are dropped; formatting commands say what their argument says", () => {
|
|
48
|
+
expect(
|
|
49
|
+
text(
|
|
50
|
+
"",
|
|
51
|
+
"As shown \\cite{a,b} and \\citep[p.~3]{c}, \\emph{this} \\textbf{is} \\texttt{code}. % a comment\nNext.% glued\nOn.",
|
|
52
|
+
),
|
|
53
|
+
).toEqual({ ok: true, value: "As shown and, this is code. Next.On." });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("abstractText — what prints nothing, a space, or a fixed word", () => {
|
|
58
|
+
it("a forced line break is a space; a label, a footnote and \\relax print nothing; the logos print their word", () => {
|
|
59
|
+
expect(
|
|
60
|
+
text(
|
|
61
|
+
"",
|
|
62
|
+
"One\\\\two\\newline three\\label{x}\\footnote{aside}\\relax{} in \\LaTeX.",
|
|
63
|
+
),
|
|
64
|
+
).toEqual({ ok: true, value: "One two three in LaTeX." });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("commands that only steer line breaking print nothing; a group in math is only grouping", () => {
|
|
68
|
+
expect(
|
|
69
|
+
text(
|
|
70
|
+
"",
|
|
71
|
+
"long\\allowbreak/word, no\\nobreak\\ break, hy\\-phen, it\\/ and ${\\sim}12{,}345$ items.",
|
|
72
|
+
),
|
|
73
|
+
).toEqual({
|
|
74
|
+
ok: true,
|
|
75
|
+
value: "long/word, no break, hyphen, it and ∼12,345 items.",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("escaped characters, \\ldots, and math that is only digits and symbols", () => {
|
|
80
|
+
expect(
|
|
81
|
+
text(
|
|
82
|
+
"",
|
|
83
|
+
"50\\% \\& more\\ldots{} up to $3\\times$ and $\\le 5$ ($p<0.05$).",
|
|
84
|
+
),
|
|
85
|
+
).toEqual({
|
|
86
|
+
ok: true,
|
|
87
|
+
value: "50% & more… up to 3× and ≤ 5 (p<0.05).",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("abstractText — refused by name and place, never guessed", () => {
|
|
93
|
+
it("a macro nobody defined", () => {
|
|
94
|
+
const r = text("", "We use \\toolname{} here.");
|
|
95
|
+
expect(r.ok ? r : r.error).toMatchObject({
|
|
96
|
+
kind: "unreadable",
|
|
97
|
+
what: "\\toolname, which no \\newcommand, \\renewcommand or \\def in the paper defines",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a macro that takes arguments — \\newcommand with [n], \\def with #1", () => {
|
|
102
|
+
const preambles = ["\\newcommand{\\two}[1]{x#1}", "\\def\\two#1{x#1}"];
|
|
103
|
+
expect(
|
|
104
|
+
map(preambles, (preamble) => {
|
|
105
|
+
const r = text(preamble, "See \\two{a}.");
|
|
106
|
+
return r.ok
|
|
107
|
+
? r
|
|
108
|
+
: {
|
|
109
|
+
kind: r.error.kind,
|
|
110
|
+
what: "what" in r.error ? r.error.what : null,
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
).toStrictEqual(
|
|
114
|
+
map(preambles, () => ({
|
|
115
|
+
kind: "unreadable",
|
|
116
|
+
what: "\\two, which takes arguments",
|
|
117
|
+
})),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("math beyond digits and symbols, and any other construct", () => {
|
|
122
|
+
const math = text("", "Accuracy $\\alpha_i$ rises.");
|
|
123
|
+
expect(math.ok ? math : math.error).toMatchObject({
|
|
124
|
+
kind: "unreadable",
|
|
125
|
+
what: "the math $\\alpha_i$",
|
|
126
|
+
});
|
|
127
|
+
const env = text("", "A list:\\begin{itemize}\\item one\\end{itemize}");
|
|
128
|
+
expect(env.ok ? env : env.error).toMatchObject({
|
|
129
|
+
kind: "unreadable",
|
|
130
|
+
what: "the itemize environment",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("abstractText — where the refusal stands, and what is not read at all", () => {
|
|
136
|
+
it("the place is the offset of the construct in the source", () => {
|
|
137
|
+
const src = doc("", "Fine. \\nope{} here.");
|
|
138
|
+
const r = latexReader.abstractText(src);
|
|
139
|
+
const at = !r.ok && r.error.kind === "unreadable" ? r.error.at : -1;
|
|
140
|
+
expect(src.slice(at, at + "\\nope".length)).toBe("\\nope");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("nothing after \\end{document} is read: neither an abstract nor a definition there", () => {
|
|
144
|
+
expect([
|
|
145
|
+
latexReader.abstractText(
|
|
146
|
+
`${doc("\\newcommand{\\x}{a}", "\\x.")}\\def\\x{b}\n\\begin{abstract}late\\end{abstract}\n`,
|
|
147
|
+
),
|
|
148
|
+
latexReader.abstractText(
|
|
149
|
+
"\\documentclass{article}\\begin{document}Body.\\end{document}\\begin{abstract}x\\end{abstract}",
|
|
150
|
+
),
|
|
151
|
+
]).toStrictEqual([
|
|
152
|
+
{ ok: true, value: "a." },
|
|
153
|
+
{ ok: false, error: { kind: "none" } },
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("no abstract environment: said so", () => {
|
|
158
|
+
expect(
|
|
159
|
+
latexReader.abstractText(
|
|
160
|
+
"\\documentclass{article}\\begin{document}Body.\\end{document}",
|
|
161
|
+
),
|
|
162
|
+
).toEqual({ ok: false, error: { kind: "none" } });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("a macro defined in terms of itself is refused, not expanded forever", () => {
|
|
166
|
+
const r = text("\\def\\loop{\\loop}", "A \\loop.");
|
|
167
|
+
expect(r.ok ? r : r.error).toMatchObject({
|
|
168
|
+
kind: "unreadable",
|
|
169
|
+
what: "\\loop, which expands into itself",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("abstractText — a macro defined more than once is refused, never resolved", () => {
|
|
175
|
+
const redefined = (preamble: string, abstract: string) => {
|
|
176
|
+
const src = doc(preamble, abstract);
|
|
177
|
+
const r = latexReader.abstractText(src);
|
|
178
|
+
return r.ok || r.error.kind !== "redefined"
|
|
179
|
+
? r
|
|
180
|
+
: { name: r.error.name, at: r.error.at.map((o) => src.slice(o, o + 12)) };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
it("\\newcommand then \\providecommand (TeX keeps the first), and a \\def again after the abstract", () => {
|
|
184
|
+
expect([
|
|
185
|
+
redefined(
|
|
186
|
+
"\\newcommand{\\x}{old}\\providecommand{\\x}{fallback}",
|
|
187
|
+
"\\x.",
|
|
188
|
+
),
|
|
189
|
+
redefined(
|
|
190
|
+
"\\def\\x{old}",
|
|
191
|
+
"\\x.\n\\end{abstract}\\def\\x{new}\\begin{abstract}",
|
|
192
|
+
),
|
|
193
|
+
]).toStrictEqual([
|
|
194
|
+
{ name: "x", at: ["\\newcommand{", "\\providecomm"] },
|
|
195
|
+
{ name: "x", at: ["\\def\\x{old}\n", "\\def\\x{new}\\"] },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("a single definition at or after \\begin{abstract} is refused at that definition, not applied backwards", () => {
|
|
200
|
+
const src = doc(
|
|
201
|
+
"",
|
|
202
|
+
"\\today.\n\\end{abstract}\\def\\today{new}\\begin{abstract}",
|
|
203
|
+
);
|
|
204
|
+
const r = latexReader.abstractText(src);
|
|
205
|
+
expect(
|
|
206
|
+
r.ok || r.error.kind !== "unreadable"
|
|
207
|
+
? r
|
|
208
|
+
: { what: r.error.what, at: src.slice(r.error.at, r.error.at + 11) },
|
|
209
|
+
).toStrictEqual({
|
|
210
|
+
what: "\\today, which the paper defines only after the abstract begins",
|
|
211
|
+
at: "\\def\\today{",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("one definition before the abstract, even in a closed group, is expanded", () => {
|
|
216
|
+
expect(text("{\\gdef\\x{once}}", "\\x.")).toEqual({
|
|
217
|
+
ok: true,
|
|
218
|
+
value: "once.",
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("abstractText — the edges of what the source may hold", () => {
|
|
224
|
+
it("malformed definitions define nothing; an author's macro reads inside math too", () => {
|
|
225
|
+
expect(
|
|
226
|
+
text(
|
|
227
|
+
"\\newcommand{oops}{x}\\newcommand{}{x}\\newcommand{\\half}\\def{y}{z}\\newcommand{\\n}{83}",
|
|
228
|
+
"We saw $\\n\\%$ of runs.",
|
|
229
|
+
),
|
|
230
|
+
).toEqual({ ok: true, value: "We saw 83% of runs." });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it.each([
|
|
234
|
+
["$a@b$", "the math $a@b$"],
|
|
235
|
+
["$x^2$", "the math $x^2$"],
|
|
236
|
+
["$1 %c\n2$", "the math $1 %c\n2$"],
|
|
237
|
+
["\\begin{equation}x\\end{equation}", "the equation environment"],
|
|
238
|
+
["\\[x\\]", "the displaymath"],
|
|
239
|
+
])("%s is refused as %s", (body, what) => {
|
|
240
|
+
const r = text("", `See ${body} here.`);
|
|
241
|
+
expect(r.ok ? r : r.error).toMatchObject({ kind: "unreadable", what });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("a node the parser gave no position is refused at -1, and \\def without arguments defines nothing", () => {
|
|
245
|
+
const tree: Ast.Root = {
|
|
246
|
+
type: "root",
|
|
247
|
+
content: [
|
|
248
|
+
{ type: "macro", content: "def" },
|
|
249
|
+
{
|
|
250
|
+
type: "environment",
|
|
251
|
+
env: "abstract",
|
|
252
|
+
content: [
|
|
253
|
+
{ type: "inlinemath", content: [{ type: "string", content: "@" }] },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
expect(abstractOfTree(tree, "")).toEqual({
|
|
259
|
+
ok: false,
|
|
260
|
+
error: { kind: "unreadable", what: "the math ", at: -1 },
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("a formatting command the parser gave no argument prints nothing", () => {
|
|
265
|
+
const tree: Ast.Root = {
|
|
266
|
+
type: "root",
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "environment",
|
|
270
|
+
env: "abstract",
|
|
271
|
+
content: [
|
|
272
|
+
{ type: "string", content: "A" },
|
|
273
|
+
{ type: "macro", content: "emph" },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
expect(abstractOfTree(tree, "")).toEqual({ ok: true, value: "A" });
|
|
279
|
+
});
|
|
280
|
+
});
|