paperlint 6.2.3 → 7.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/CONTRIBUTING.md +4 -2
- package/README.md +10 -3
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- package/dist/adapters/latex/conditionals.d.ts +21 -0
- package/dist/adapters/latex/conditionals.d.ts.map +1 -0
- package/dist/adapters/latex/conditionals.js +75 -0
- package/dist/adapters/latex/conditionals.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +10 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -0
- package/dist/adapters/latex/headings.js +129 -0
- package/dist/adapters/latex/headings.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +3 -2
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/heading-case.d.ts +100 -0
- package/dist/domain/heading-case.d.ts.map +1 -0
- package/dist/domain/heading-case.js +365 -0
- package/dist/domain/heading-case.js.map +1 -0
- package/dist/domain/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.js.map +1 -0
- package/dist/domain/tex-document.d.ts +32 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +11 -0
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +16 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/facts-file.d.ts +3 -3
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +3 -3
- package/dist/facts-file.js.map +1 -1
- package/dist/folder-venue-rule.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- package/dist/heading-case.d.ts +48 -0
- package/dist/heading-case.d.ts.map +1 -0
- package/dist/heading-case.js +191 -0
- package/dist/heading-case.js.map +1 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- package/dist/preset-card-rule.d.ts +100 -0
- package/dist/preset-card-rule.d.ts.map +1 -0
- package/dist/preset-card-rule.js +221 -0
- package/dist/preset-card-rule.js.map +1 -0
- package/dist/presets.d.ts +43 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +130 -63
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +36 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +30 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -8
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +30 -11
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +64 -53
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-checks.mjs +31 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +8 -2
- package/presets/acm-sigconf.jsonc +5 -0
- package/presets/acm-sigconf.md +43 -0
- package/presets/agenticdev.jsonc +27 -6
- package/presets/agenticdev.md +151 -0
- package/presets/aidc.jsonc +12 -0
- package/presets/aidc.md +75 -0
- package/presets/aisec.jsonc +5 -1
- package/presets/aisec.md +117 -0
- package/presets/ieee-conference.jsonc +5 -0
- package/presets/ieee-conference.md +38 -0
- package/presets/realm.jsonc +7 -2
- package/presets/realm.md +166 -0
- package/presets/tex/agenticdev.tex +1 -1
- package/presets/tex-base.jsonc +6 -0
- package/presets/tex-base.md +30 -0
- package/presets/venue-profile.schema.json +57 -2
- package/skills/find-venue/SKILL.md +6 -5
- package/skills/find-venue/SKILL.md.spec.ts +5 -4
- package/skills/paper-pipeline/SKILL.md +5 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +27 -22
- package/skills/submit-paper/SKILL.md.spec.ts +26 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- package/src/adapters/latex/conditionals.ts +130 -0
- package/src/adapters/latex/headings.ts +163 -0
- package/src/adapters/latex/index.test.ts +65 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +3 -2
- package/src/build.ts +2 -2
- package/src/cli.test.ts +155 -3
- package/src/cli.ts +161 -25
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +121 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +135 -10
- package/src/new-paper.ts +38 -7
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/latex.ts +7 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +212 -37
- package/src/presets.ts +183 -71
- package/src/rule-cases.test.ts +147 -42
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +497 -0
- package/src/submission.ts +311 -0
- package/src/talk-rules.test.ts +6 -3
- package/src/tex-requirements.harness.ts +25 -11
- package/src/tex-requirements.test.ts +127 -1
- package/src/tex-requirements.ts +78 -8
- package/src/tex-venue-rules.test.ts +36 -22
- package/src/tex-venue-rules.ts +14 -26
- package/src/types.ts +13 -0
- package/src/venue-rules.test.ts +7 -6
- package/templates/paper/paper.tex +1 -1
- package/skills/submit-paper/references/venues/agenticdev.md +0 -171
- package/skills/submit-paper/references/venues/aisec.md +0 -105
- package/skills/submit-paper/references/venues/realm.md +0 -155
package/CONTRIBUTING.md
CHANGED
|
@@ -175,8 +175,10 @@ A venue is a **preset**, a JSONC file in `presets/`, validated by
|
|
|
175
175
|
A venue on a template with no family yet stands alone (`template`, `tex`, `format`) — or, better,
|
|
176
176
|
add the family first: measured on a real template build (banal + pdf.js), not copied from
|
|
177
177
|
documentation ([#88](https://github.com/zernie/paperlint/issues/88)).
|
|
178
|
-
2. **The card.** `
|
|
179
|
-
what the form asks.
|
|
178
|
+
2. **The card.** `presets/<name>.md`, beside the preset — prose about the venue: deadlines, tracks, the blind model,
|
|
179
|
+
what the form asks. Lint fails while a preset has no card; the rules section between the generated
|
|
180
|
+
markers is written by `npm run build && npx eslint --fix presets/` (the config loads the rule from
|
|
181
|
+
`dist/`).
|
|
180
182
|
3. **A test.** A case in `src/presets.test.ts` that `paperlint:<name>` resolves over its family with
|
|
181
183
|
the kinds you declared; `src/tex-requirements.harness.ts` already checks every shipped preset
|
|
182
184
|
against the schema.
|
package/README.md
CHANGED
|
@@ -103,9 +103,12 @@ REALM.
|
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
3. Create a paper. `--venue` names its venue preset from the table above, `--kind` which of its page
|
|
106
|
-
limits applies:
|
|
106
|
+
limits applies. Name the folder after the work, not the venue: a rejected paper moves to another
|
|
107
|
+
venue and keeps its folder, so `new` refuses a name like `aisec-2026` (`--allow-venue-name` when
|
|
108
|
+
the word really is the work's):
|
|
107
109
|
|
|
108
110
|
<!-- `vigiles:symbol src/new-paper.ts#newPaper` — `npm run check` fails if this function is renamed or removed. -->
|
|
111
|
+
<!-- `vigiles:symbol src/new-paper.ts#ALLOW_VENUE_NAME` — the flag's one spelling; `npm run check` fails if it is renamed or removed. -->
|
|
109
112
|
|
|
110
113
|
```sh
|
|
111
114
|
npx paperlint new my-paper --venue agenticdev --kind short
|
|
@@ -133,8 +136,8 @@ REALM.
|
|
|
133
136
|
|
|
134
137
|
```text
|
|
135
138
|
papers/my-paper/paper.tex
|
|
136
|
-
1:1 error 1 column(s),
|
|
137
|
-
1:1 error no font starts with `LinLibertine` (body text of
|
|
139
|
+
1:1 error 1 column(s), AgenticDev requires 2 — the wrong document class or class option pdf/geometry
|
|
140
|
+
1:1 error no font starts with `LinLibertine` (body text of AgenticDev); the PDF has: CMR10, … pdf/fonts
|
|
138
141
|
8:4 warning `§` instead of the word «Section» — `paperlint lint --fix` writes it paper/section-word
|
|
139
142
|
```
|
|
140
143
|
|
|
@@ -161,6 +164,8 @@ paperlint finds a paper by its folder: `<papersDir>/<name>/`, with the main file
|
|
|
161
164
|
| `npx paperlint lint` | runs every check over your papers; `--fix` fixes what can be fixed |
|
|
162
165
|
| `npx paperlint toolchain` | installs TeX Live with the packages your venues need (~270 MB, ~3 min, once) |
|
|
163
166
|
| `npx paperlint doctor` | checks the setup and exits non-zero if something is miswired |
|
|
167
|
+
| `npx paperlint submission show <paper>` | the paper's submission on the venue's portal, and whether it holds your build |
|
|
168
|
+
| `npx paperlint submission update <paper>` | checks a new PDF/abstract against the portal; `--save` sends it for real |
|
|
164
169
|
| `npx paperlint --help` | every command and flag |
|
|
165
170
|
|
|
166
171
|
## 🧠 Skills
|
|
@@ -175,6 +180,7 @@ what it does — "is this idea worth a paper?", "find me a venue for this". `ini
|
|
|
175
180
|
- **A hostile review before the real one.** `paper-adversarial-review`
|
|
176
181
|
- **A ready / not ready verdict before you submit**, worst problem first. `harden-paper`
|
|
177
182
|
- **Where your paper stands**, measured from the real build. `paper-status`
|
|
183
|
+
- **The PDF and abstract replaced on HotCRP**, dry run first, and proof the portal holds your build. `submission-portal`
|
|
178
184
|
|
|
179
185
|
Every skill, by stage: [`docs/skills.md`](docs/skills.md).
|
|
180
186
|
|
|
@@ -313,6 +319,7 @@ Only `paperlint lint --fix`, and only three rules: `paper/section-word` (`§`
|
|
|
313
319
|
- [`docs/configuration.md`](docs/configuration.md) — every setting, how `build` compiles, using your own ESLint
|
|
314
320
|
- [`docs/rules.md`](docs/rules.md) — every check, venue presets, recording a submitted PDF
|
|
315
321
|
- [`docs/references.md`](docs/references.md) — the reference check, and the cache file to commit
|
|
322
|
+
- [`docs/submission.md`](docs/submission.md) — reading and updating a submission on the venue's portal (HotCRP)
|
|
316
323
|
- [`docs/optional-rules.md`](docs/optional-rules.md) — checks only some venues need
|
|
317
324
|
- [`docs/toolchain.md`](docs/toolchain.md) — TeX Live, and Banal (HotCRP's page-geometry checker, GPL)
|
|
318
325
|
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how the package is tested and released, and adding a venue to it
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP, as the `SubmissionPortal` port: its REST API read and written over `fetch`, its answers
|
|
3
|
+
* parsed here. Everything that changes if HotCRP's API does lives in this folder.
|
|
4
|
+
*/
|
|
5
|
+
export { CONTENT_FIELD, changeObject, hotcrpPortal, type HotcrpOptions, } from "./portal.io.ts";
|
|
6
|
+
export { parseShow, parseUpdate, sha256OfHash } from "./response.ts";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/hotcrp/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP, as the `SubmissionPortal` port: its REST API read and written over `fetch`, its answers
|
|
3
|
+
* parsed here. Everything that changes if HotCRP's API does lives in this folder.
|
|
4
|
+
*/
|
|
5
|
+
export { CONTENT_FIELD, changeObject, hotcrpPortal, } from "./portal.io.js";
|
|
6
|
+
export { parseShow, parseUpdate, sha256OfHash } from "./response.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/hotcrp/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,GAEb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SubmissionChange } from "../../domain/submission.ts";
|
|
2
|
+
import type { SubmissionPortal } from "../../ports/submission-portal.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The form field the PDF travels in, and the name the `json` object's `content_file` refers to it
|
|
5
|
+
* by. 🔴 NO DOT, SPACE OR BRACKET: HotCRP looks the name up in PHP's `$_FILES`, and PHP renames a
|
|
6
|
+
* dot or a space in a field name to `_`. The documented `-F paper.pdf=@paper.pdf` therefore arrives
|
|
7
|
+
* as `paper_pdf`, the lookup of `paper.pdf` misses, and HotCRP answers "Ignored attempt to upload
|
|
8
|
+
* document without any content" with `valid: false` — measured on a real HotCRP, 2026-10-03.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CONTENT_FIELD = "paperlint_pdf";
|
|
11
|
+
export interface HotcrpOptions {
|
|
12
|
+
/** The site's base URL, no trailing slash: `https://example.org`. */
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly token: string;
|
|
15
|
+
}
|
|
16
|
+
/** The submission object a change sends: only the fields it changes. */
|
|
17
|
+
export declare function changeObject(id: number, c: SubmissionChange): Readonly<Record<string, unknown>>;
|
|
18
|
+
/** HotCRP at `o.url`, read and changed with `o.token`. */
|
|
19
|
+
export declare function hotcrpPortal(o: HotcrpOptions): SubmissionPortal;
|
|
20
|
+
//# sourceMappingURL=portal.io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portal.io.d.ts","sourceRoot":"","sources":["../../../src/adapters/hotcrp/portal.io.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAGzE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAM7C,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAmCD,wEAAwE;AACxE,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,CAAC,EAAE,gBAAgB,GAClB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAQnC;AAmBD,0DAA0D;AAC1D,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,GAAG,gBAAgB,CAsB/D"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP's REST API (`<site>/api/paper`) as the `SubmissionPortal` port, over Node's `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Read: `GET /api/paper?p=<id>&word_limit=hard` (scope `submeta:read`; `word_limit=hard` returns the
|
|
5
|
+
* whole abstract instead of one cut at the soft limit).
|
|
6
|
+
*
|
|
7
|
+
* Change: `POST /api/paper?p=<id>` as multipart/form-data — a `json` field holding a submission
|
|
8
|
+
* object, and the PDF as a file part whose field name is the object's `submission.content_file`
|
|
9
|
+
* (the documented form: `curl -F "json=<data.json" -F paper.pdf=@paper.pdf SITEURL/api/paper`).
|
|
10
|
+
* Without `save` the request carries `dry_run=1`: HotCRP checks the change and keeps nothing.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 THE TOKEN goes in the `Authorization` header and nowhere else: not into a URL, a message, or an
|
|
13
|
+
* error. A failure is described by HTTP status, HotCRP's own messages, or the network error's text.
|
|
14
|
+
*/
|
|
15
|
+
import { messageOf } from "../../domain/text.js";
|
|
16
|
+
import { err } from "../../domain/result.js";
|
|
17
|
+
import { parseShow, parseUpdate } from "./response.js";
|
|
18
|
+
/**
|
|
19
|
+
* The form field the PDF travels in, and the name the `json` object's `content_file` refers to it
|
|
20
|
+
* by. 🔴 NO DOT, SPACE OR BRACKET: HotCRP looks the name up in PHP's `$_FILES`, and PHP renames a
|
|
21
|
+
* dot or a space in a field name to `_`. The documented `-F paper.pdf=@paper.pdf` therefore arrives
|
|
22
|
+
* as `paper_pdf`, the lookup of `paper.pdf` misses, and HotCRP answers "Ignored attempt to upload
|
|
23
|
+
* document without any content" with `valid: false` — measured on a real HotCRP, 2026-10-03.
|
|
24
|
+
*/
|
|
25
|
+
export const CONTENT_FIELD = "paperlint_pdf";
|
|
26
|
+
/** Uploads take a while on a slow link; a read should not. */
|
|
27
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
28
|
+
const WRITE_TIMEOUT_MS = 300_000;
|
|
29
|
+
/** One request → HTTP status and parsed JSON body, or why there is neither. */
|
|
30
|
+
async function call(url, init, token, timeoutMs) {
|
|
31
|
+
try {
|
|
32
|
+
const r = await fetch(url, {
|
|
33
|
+
...init,
|
|
34
|
+
headers: { Authorization: `bearer ${token}`, Accept: "application/json" },
|
|
35
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
36
|
+
});
|
|
37
|
+
const text = await r.text();
|
|
38
|
+
try {
|
|
39
|
+
return { ok: true, value: { status: r.status, body: JSON.parse(text) } };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return err({
|
|
43
|
+
kind: "malformed",
|
|
44
|
+
httpStatus: r.status,
|
|
45
|
+
detail: `the answer is not JSON (${r.headers.get("content-type") ?? "no content type"})`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return err({ kind: "unreachable", detail: messageOf(e) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The submission object a change sends: only the fields it changes. */
|
|
54
|
+
export function changeObject(id, c) {
|
|
55
|
+
return {
|
|
56
|
+
object: "paper",
|
|
57
|
+
pid: id,
|
|
58
|
+
...(c.pdf === null ? {} : { submission: { content_file: CONTENT_FIELD } }),
|
|
59
|
+
...(c.abstract === null ? {} : { abstract: c.abstract }),
|
|
60
|
+
...(c.submit ? { status: "submitted" } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function formOf(id, c) {
|
|
64
|
+
const form = new FormData();
|
|
65
|
+
form.append("json", JSON.stringify(changeObject(id, c)));
|
|
66
|
+
if (c.pdf !== null)
|
|
67
|
+
form.append(CONTENT_FIELD, new Blob([new Uint8Array(c.pdf.bytes)], { type: "application/pdf" }), c.pdf.name);
|
|
68
|
+
return form;
|
|
69
|
+
}
|
|
70
|
+
const endpoint = (base, params) => `${base}/api/paper?${new URLSearchParams(params).toString()}`;
|
|
71
|
+
/** HotCRP at `o.url`, read and changed with `o.token`. */
|
|
72
|
+
export function hotcrpPortal(o) {
|
|
73
|
+
return {
|
|
74
|
+
async show(id) {
|
|
75
|
+
const url = endpoint(o.url, { p: String(id), word_limit: "hard" });
|
|
76
|
+
const a = await call(url, { method: "GET" }, o.token, READ_TIMEOUT_MS);
|
|
77
|
+
return a.ok ? parseShow(a.value.status, a.value.body) : a;
|
|
78
|
+
},
|
|
79
|
+
async update(id, change, { save }) {
|
|
80
|
+
const url = endpoint(o.url, {
|
|
81
|
+
p: String(id),
|
|
82
|
+
...(save ? {} : { dry_run: "1" }),
|
|
83
|
+
});
|
|
84
|
+
const body = formOf(id, change);
|
|
85
|
+
const a = await call(url, { method: "POST", body }, o.token, WRITE_TIMEOUT_MS);
|
|
86
|
+
return a.ok ? parseUpdate(a.value.status, a.value.body) : a;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=portal.io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portal.io.js","sourceRoot":"","sources":["../../../src/adapters/hotcrp/portal.io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,GAAG,EAAe,MAAM,wBAAwB,CAAC;AAM1D,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C,8DAA8D;AAC9D,MAAM,eAAe,GAAG,MAAM,CAAC;AAC/B,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAajC,+EAA+E;AAC/E,KAAK,UAAU,IAAI,CACjB,GAAW,EACX,IAAqE,EACrE,KAAa,EACb,SAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACzB,GAAG,IAAI;YACP,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;YACzE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,GAAG,CAAC;gBACT,IAAI,EAAE,WAAW;gBACjB,UAAU,EAAE,CAAC,CAAC,MAAM;gBACpB,MAAM,EAAE,2BAA2B,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,iBAAiB,GAAG;aACzF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAC1B,EAAU,EACV,CAAmB;IAEnB,OAAO;QACL,MAAM,EAAE,OAAO;QACf,GAAG,EAAE,EAAE;QACP,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,CAAC;QAC1E,GAAG,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;QACxD,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7C,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,EAAU,EAAE,CAAmB;IAC7C,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI;QAChB,IAAI,CAAC,MAAM,CACT,aAAa,EACb,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EACpE,CAAC,CAAC,GAAG,CAAC,IAAI,CACX,CAAC;IACJ,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,QAAQ,GAAG,CACf,IAAY,EACZ,MAAwC,EAChC,EAAE,CAAC,GAAG,IAAI,cAAc,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;AAE3E,0DAA0D;AAC1D,MAAM,UAAU,YAAY,CAAC,CAAgB;IAC3C,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,EAAE;YACX,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;YACnE,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;YACvE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;YAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE;gBAC1B,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;aAClC,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;YAChC,MAAM,CAAC,GAAG,MAAM,IAAI,CAClB,GAAG,EACH,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EACxB,CAAC,CAAC,KAAK,EACP,gBAAgB,CACjB,CAAC;YACF,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Result } from "../../domain/result.ts";
|
|
2
|
+
import { type Sha256 } from "../../domain/sha256.ts";
|
|
3
|
+
import type { PortalFailure, SubmissionView, UpdateOutcome } from "../../domain/submission.ts";
|
|
4
|
+
/** HotCRP's `sha2-<hex>` → the sha256; null for any other hash it may name. */
|
|
5
|
+
export declare function sha256OfHash(hash: string): Sha256 | null;
|
|
6
|
+
/** `GET /api/paper` → the submission, or why there is none. */
|
|
7
|
+
export declare function parseShow(httpStatus: number, body: unknown): Result<SubmissionView, PortalFailure>;
|
|
8
|
+
/**
|
|
9
|
+
* `POST /api/paper` → the outcome, valid or not. A body carrying `valid` is HotCRP's verdict on
|
|
10
|
+
* the change whatever the HTTP status; the caller decides the exit from both.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseUpdate(httpStatus: number, body: unknown): Result<UpdateOutcome, PortalFailure>;
|
|
13
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../../src/adapters/hotcrp/response.ts"],"names":[],"mappings":"AAUA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAe,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,KAAK,EAEV,aAAa,EAEb,cAAc,EACd,aAAa,EACd,MAAM,4BAA4B,CAAC;AA6CpC,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGxD;AAkFD,+DAA+D;AAC/D,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,cAAc,EAAE,aAAa,CAAC,CAMvC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,aAAa,EAAE,aAAa,CAAC,CAYtC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP's answers, parsed at the boundary into paperlint's submission values. Pure.
|
|
3
|
+
*
|
|
4
|
+
* Only the fields the command uses are read; every other key HotCRP sends is dropped here. The field
|
|
5
|
+
* names are HotCRP's (`devel/openapi.json` and `devel/apidoc/submissions.md` in kohler/hotcrp):
|
|
6
|
+
* `GET /api/paper` answers `{ ok, paper, message_list? }`, `POST /api/paper` answers
|
|
7
|
+
* `{ ok, valid, change_list, message_list, dry_run?, pid }`. A document's `hash` is
|
|
8
|
+
* `sha2-<64 hex>` when HotCRP stored a sha256; timestamps are Unix seconds.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { err, ok } from "../../domain/result.js";
|
|
12
|
+
import { parseSha256 } from "../../domain/sha256.js";
|
|
13
|
+
const Message = z.object({
|
|
14
|
+
message: z.string().optional(),
|
|
15
|
+
field: z.string().optional(),
|
|
16
|
+
status: z.number().optional(),
|
|
17
|
+
});
|
|
18
|
+
const Messages = z.array(Message).optional();
|
|
19
|
+
const Document = z.object({
|
|
20
|
+
hash: z.string().optional(),
|
|
21
|
+
size: z.number().optional(),
|
|
22
|
+
mimetype: z.string().optional(),
|
|
23
|
+
timestamp: z.number().optional(),
|
|
24
|
+
});
|
|
25
|
+
const Paper = z.object({
|
|
26
|
+
pid: z.number(),
|
|
27
|
+
status: z.string(),
|
|
28
|
+
title: z.string().optional(),
|
|
29
|
+
abstract: z.string().optional(),
|
|
30
|
+
paper_type: z.unknown().optional(),
|
|
31
|
+
topics: z.array(z.string()).optional(),
|
|
32
|
+
submission: Document.nullable().optional(),
|
|
33
|
+
submitted_at: z.number().optional(),
|
|
34
|
+
modified_at: z.number().optional(),
|
|
35
|
+
});
|
|
36
|
+
const ShowBody = z.object({
|
|
37
|
+
ok: z.literal(true),
|
|
38
|
+
paper: Paper,
|
|
39
|
+
message_list: Messages,
|
|
40
|
+
});
|
|
41
|
+
const UpdateBody = z.object({
|
|
42
|
+
valid: z.boolean(),
|
|
43
|
+
change_list: z.array(z.string()).optional(),
|
|
44
|
+
message_list: Messages,
|
|
45
|
+
dry_run: z.boolean().optional(),
|
|
46
|
+
});
|
|
47
|
+
const Envelope = z.object({ message_list: Messages });
|
|
48
|
+
const SHA2 = /^sha2-([0-9a-f]{64})$/;
|
|
49
|
+
/** HotCRP's `sha2-<hex>` → the sha256; null for any other hash it may name. */
|
|
50
|
+
export function sha256OfHash(hash) {
|
|
51
|
+
const hex = SHA2.exec(hash)?.[1];
|
|
52
|
+
return hex === undefined ? null : parseSha256(hex);
|
|
53
|
+
}
|
|
54
|
+
/** HotCRP prefixes a message with its format (`<0>` plain text, `<5>` HTML); the text is the rest. */
|
|
55
|
+
const textOf = (message) => (message ?? "").replace(/^<\d+>/, "").trim();
|
|
56
|
+
/** The messages that say something: HotCRP also sends entries with no text, which print as blanks. */
|
|
57
|
+
const messagesOf = (list) => (list ?? [])
|
|
58
|
+
.map((m) => ({
|
|
59
|
+
message: textOf(m.message),
|
|
60
|
+
field: m.field ?? null,
|
|
61
|
+
status: m.status ?? null,
|
|
62
|
+
}))
|
|
63
|
+
.filter((m) => m.message !== "");
|
|
64
|
+
const documentOf = (d) => d?.hash === undefined
|
|
65
|
+
? null
|
|
66
|
+
: {
|
|
67
|
+
sha256: sha256OfHash(d.hash),
|
|
68
|
+
hash: d.hash,
|
|
69
|
+
size: d.size ?? null,
|
|
70
|
+
mimetype: d.mimetype ?? null,
|
|
71
|
+
uploadedAt: d.timestamp ?? null,
|
|
72
|
+
};
|
|
73
|
+
/** A paper type as text: HotCRP sends the option's name, or a value of another shape. */
|
|
74
|
+
const typeText = (v) => v === undefined || v === null
|
|
75
|
+
? null
|
|
76
|
+
: typeof v === "string"
|
|
77
|
+
? v
|
|
78
|
+
: JSON.stringify(v);
|
|
79
|
+
function viewOf(b) {
|
|
80
|
+
const p = b.paper;
|
|
81
|
+
return {
|
|
82
|
+
id: p.pid,
|
|
83
|
+
status: p.status,
|
|
84
|
+
title: p.title ?? null,
|
|
85
|
+
paperType: typeText(p.paper_type),
|
|
86
|
+
topics: p.topics ?? [],
|
|
87
|
+
abstract: p.abstract ?? null,
|
|
88
|
+
pdf: documentOf(p.submission),
|
|
89
|
+
submittedAt: p.submitted_at ?? null,
|
|
90
|
+
modifiedAt: p.modified_at ?? null,
|
|
91
|
+
messages: messagesOf(b.message_list),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const is2xx = (status) => status >= 200 && status < 300;
|
|
95
|
+
const StatusCode = z.object({ status_code: z.number() });
|
|
96
|
+
/**
|
|
97
|
+
* The status HotCRP meant. Its error envelope repeats the status in the body (`status_code: 401`
|
|
98
|
+
* beside `ok: false`, recorded in `cassette/get-noauth.json`); when something between us and HotCRP
|
|
99
|
+
* turns the transport status into a 2xx, the body's code still decides.
|
|
100
|
+
*/
|
|
101
|
+
function statusOf(transport, body) {
|
|
102
|
+
const c = StatusCode.safeParse(body);
|
|
103
|
+
return is2xx(transport) && c.success ? c.data.status_code : transport;
|
|
104
|
+
}
|
|
105
|
+
/** A body that is no answer of HotCRP's: the portal's messages when it sent any, else "malformed". */
|
|
106
|
+
function refusal(httpStatus, body, what) {
|
|
107
|
+
const e = Envelope.safeParse(body);
|
|
108
|
+
const messages = e.success ? messagesOf(e.data.message_list) : [];
|
|
109
|
+
return messages.length > 0 || !is2xx(httpStatus)
|
|
110
|
+
? { kind: "refused", httpStatus, messages }
|
|
111
|
+
: { kind: "malformed", httpStatus, detail: what };
|
|
112
|
+
}
|
|
113
|
+
/** `GET /api/paper` → the submission, or why there is none. */
|
|
114
|
+
export function parseShow(httpStatus, body) {
|
|
115
|
+
const status = statusOf(httpStatus, body);
|
|
116
|
+
const b = ShowBody.safeParse(body);
|
|
117
|
+
return b.success && is2xx(status)
|
|
118
|
+
? ok(viewOf(b.data))
|
|
119
|
+
: err(refusal(status, body, "the answer has no submission in `paper`"));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `POST /api/paper` → the outcome, valid or not. A body carrying `valid` is HotCRP's verdict on
|
|
123
|
+
* the change whatever the HTTP status; the caller decides the exit from both.
|
|
124
|
+
*/
|
|
125
|
+
export function parseUpdate(httpStatus, body) {
|
|
126
|
+
const status = statusOf(httpStatus, body);
|
|
127
|
+
const b = UpdateBody.safeParse(body);
|
|
128
|
+
if (!b.success)
|
|
129
|
+
return err(refusal(status, body, "the answer has no `valid`"));
|
|
130
|
+
return ok({
|
|
131
|
+
httpStatus: status,
|
|
132
|
+
valid: b.data.valid,
|
|
133
|
+
changes: b.data.change_list ?? [],
|
|
134
|
+
messages: messagesOf(b.data.message_list),
|
|
135
|
+
dryRun: b.data.dry_run ?? false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../../src/adapters/hotcrp/response.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `\iffalse … \fi`: text TeX never reads. Authors park old titles, sections and paragraphs there,
|
|
3
|
+
* and every reader of the tree — the outline, the rendered text, the headings the case judge reads,
|
|
4
|
+
* the includes — would otherwise take them for the paper. So the branch is cut out of the tree once,
|
|
5
|
+
* when the source is parsed (`parseLatex`), rather than pruned per walk like `hidden.ts` prunes: a
|
|
6
|
+
* walk can skip a node, but the rendered text reads the strings of a node list directly, and they
|
|
7
|
+
* would come back.
|
|
8
|
+
*
|
|
9
|
+
* What is cut: from `\iffalse` to its matching `\fi`, both included, counting the conditionals
|
|
10
|
+
* nested in between. An `\else` at the same depth ends the dead part: what follows it is read, and
|
|
11
|
+
* the matching `\fi` is cut. A conditional is a macro whose name starts with `if`, except
|
|
12
|
+
* `\ifthenelse` (the `ifthen` package's command, which takes arguments and has no `\fi`). An
|
|
13
|
+
* `\iffalse` with no `\fi` cuts to the end of its list, as TeX would read to the end of the file.
|
|
14
|
+
*
|
|
15
|
+
* Only `\iffalse` is decided. `\iftrue`, `\if\venue1` and every switch made with `\newif` depend on
|
|
16
|
+
* a value this reader does not compute, so both of their branches stay.
|
|
17
|
+
*/
|
|
18
|
+
import type * as Ast from "@unified-latex/unified-latex-types";
|
|
19
|
+
/** The tree without what `\iffalse … \fi` hides; the same tree when the source has no `\iffalse`. */
|
|
20
|
+
export declare const withoutFalseBranches: (src: string, root: Readonly<Ast.Root>) => Ast.Root;
|
|
21
|
+
//# sourceMappingURL=conditionals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditionals.d.ts","sourceRoot":"","sources":["../../../src/adapters/latex/conditionals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,KAAK,GAAG,MAAM,oCAAoC,CAAC;AAwG/D,qGAAqG;AACrG,eAAO,MAAM,oBAAoB,GAC/B,KAAK,MAAM,EACX,MAAM,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAEvB,GAAG,CAAC,IAGG,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const LIVE = { kind: "live" };
|
|
2
|
+
const isMacro = (n, name) => n.type === "macro" && n.content === name;
|
|
3
|
+
/** A macro that opens a conditional TeX closes with `\fi`. */
|
|
4
|
+
const opensConditional = (n) => n.type === "macro" &&
|
|
5
|
+
n.content.startsWith("if") &&
|
|
6
|
+
n.content !== "ifthenelse";
|
|
7
|
+
/** Inside the dead branch: everything is cut; nesting is counted, `\else` at depth 1 ends it. */
|
|
8
|
+
function stepDead(depth, n) {
|
|
9
|
+
if (opensConditional(n))
|
|
10
|
+
return { mode: { kind: "dead", depth: depth + 1 }, keep: false };
|
|
11
|
+
if (isMacro(n, "fi"))
|
|
12
|
+
return {
|
|
13
|
+
mode: depth === 1 ? LIVE : { kind: "dead", depth: depth - 1 },
|
|
14
|
+
keep: false,
|
|
15
|
+
};
|
|
16
|
+
if (isMacro(n, "else") && depth === 1)
|
|
17
|
+
return { mode: { kind: "else", depth }, keep: false };
|
|
18
|
+
return { mode: { kind: "dead", depth }, keep: false };
|
|
19
|
+
}
|
|
20
|
+
/** In the `\else` branch: everything is kept but the `\fi` that closes the `\iffalse`. */
|
|
21
|
+
function stepElse(depth, n) {
|
|
22
|
+
if (opensConditional(n))
|
|
23
|
+
return { mode: { kind: "else", depth: depth + 1 }, keep: true };
|
|
24
|
+
if (isMacro(n, "fi"))
|
|
25
|
+
return depth === 1
|
|
26
|
+
? { mode: LIVE, keep: false }
|
|
27
|
+
: { mode: { kind: "else", depth: depth - 1 }, keep: true };
|
|
28
|
+
return { mode: { kind: "else", depth }, keep: true };
|
|
29
|
+
}
|
|
30
|
+
function step(mode, n) {
|
|
31
|
+
switch (mode.kind) {
|
|
32
|
+
case "live":
|
|
33
|
+
return isMacro(n, "iffalse")
|
|
34
|
+
? { mode: { kind: "dead", depth: 1 }, keep: false }
|
|
35
|
+
: { mode, keep: true };
|
|
36
|
+
case "dead":
|
|
37
|
+
return stepDead(mode.depth, n);
|
|
38
|
+
case "else":
|
|
39
|
+
return stepElse(mode.depth, n);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** A node list without its `\iffalse` branches, each kept node cleaned the same way. */
|
|
43
|
+
// eslint-disable-next-line functional/prefer-immutable-types -- the tree's own content type is a mutable array
|
|
44
|
+
function liveList(nodes) {
|
|
45
|
+
const { kept } = nodes.reduce((acc, n) => {
|
|
46
|
+
const next = step(acc.mode, n);
|
|
47
|
+
return {
|
|
48
|
+
mode: next.mode,
|
|
49
|
+
kept: next.keep ? [...acc.kept, liveNode(n)] : acc.kept,
|
|
50
|
+
};
|
|
51
|
+
}, { mode: LIVE, kept: [] });
|
|
52
|
+
return [...kept];
|
|
53
|
+
}
|
|
54
|
+
// eslint-disable-next-line functional/prefer-immutable-types -- the tree's own argument type is mutable
|
|
55
|
+
const liveArgs = (args) => args.map((a) => ({ ...a, content: liveList(a.content) }));
|
|
56
|
+
/** A node with its content and its arguments' content cleaned; a node with neither, as it is. */
|
|
57
|
+
// eslint-disable-next-line functional/prefer-immutable-types -- the tree's own node type is mutable
|
|
58
|
+
function liveNode(n) {
|
|
59
|
+
if (n.type === "macro")
|
|
60
|
+
return n.args === undefined ? n : { ...n, args: liveArgs(n.args) };
|
|
61
|
+
if (n.type === "environment" || n.type === "mathenv")
|
|
62
|
+
return {
|
|
63
|
+
...n,
|
|
64
|
+
content: liveList(n.content),
|
|
65
|
+
...(n.args === undefined ? {} : { args: liveArgs(n.args) }),
|
|
66
|
+
};
|
|
67
|
+
if (n.type === "group" || n.type === "inlinemath" || n.type === "displaymath")
|
|
68
|
+
return { ...n, content: liveList(n.content) };
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
/** The tree without what `\iffalse … \fi` hides; the same tree when the source has no `\iffalse`. */
|
|
72
|
+
export const withoutFalseBranches = (src, root) => src.includes("\\iffalse")
|
|
73
|
+
? { ...root, content: liveList(root.content) }
|
|
74
|
+
: root;
|
|
75
|
+
//# sourceMappingURL=conditionals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditionals.js","sourceRoot":"","sources":["../../../src/adapters/latex/conditionals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TitledHeading } from "../../domain/tex-document.ts";
|
|
2
|
+
import type { ParsedTex } from "./parse.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The paper's `\title` and every `\section`, `\subsection`, `\subsubsection`, `\paragraph` and
|
|
5
|
+
* `\subparagraph`, starred or not, each with its optional short title, in document order. What the
|
|
6
|
+
* PDF never shows is pruned (`isUnrendered`), and so is a macro definition's body: a heading in a
|
|
7
|
+
* comment environment or in a definition is not a heading where it is written.
|
|
8
|
+
*/
|
|
9
|
+
export declare const headingsOf: (t: ParsedTex) => readonly TitledHeading[];
|
|
10
|
+
//# sourceMappingURL=headings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headings.d.ts","sourceRoot":"","sources":["../../../src/adapters/latex/headings.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAKV,aAAa,EACd,MAAM,8BAA8B,CAAC;AActC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAyH5C;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,SAAS,KAAG,SAAS,aAAa,EAGG,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paper's `\title` and the titles of its headings as the case judge reads them: each as one run
|
|
3
|
+
* of text, every character with the source offset it came from.
|
|
4
|
+
*
|
|
5
|
+
* What a title says is its strings and spaces, and the words a formatting command wraps
|
|
6
|
+
* (`\emph{very}` says "very"). A label, a footnote or an index entry says nothing in the title and
|
|
7
|
+
* is left out. Anything else — math, `\texttt`, `\cite`, `\textsc`, an accent, an environment — is
|
|
8
|
+
* something the reader sees and the judge does not read, so it stands as `OPAQUE`, one character,
|
|
9
|
+
* where it is: the judge then knows a word is first or last only when nothing stands before or
|
|
10
|
+
* after it, and never reads a word that touches one.
|
|
11
|
+
*/
|
|
12
|
+
import { hasAtLeast } from "remeda";
|
|
13
|
+
import { LINE_BREAK, OPAQUE } from "../../domain/tex-document.js";
|
|
14
|
+
import { isDefinition, isUnrendered } from "./hidden.js";
|
|
15
|
+
import { inPlace, isNode, mandatory, optional, placeOf, stringText, visited, } from "./nodes.js";
|
|
16
|
+
const LEVELS = new Map([
|
|
17
|
+
["section", "section"],
|
|
18
|
+
["subsection", "subsection"],
|
|
19
|
+
["subsubsection", "subsubsection"],
|
|
20
|
+
["paragraph", "paragraph"],
|
|
21
|
+
["subparagraph", "subparagraph"],
|
|
22
|
+
]);
|
|
23
|
+
/** Formatting commands: they say what their argument says. */
|
|
24
|
+
const TRANSPARENT = new Set([
|
|
25
|
+
"emph",
|
|
26
|
+
"textit",
|
|
27
|
+
"textbf",
|
|
28
|
+
"textsl",
|
|
29
|
+
"textup",
|
|
30
|
+
"textmd",
|
|
31
|
+
"textnormal",
|
|
32
|
+
"textsf",
|
|
33
|
+
"textrm",
|
|
34
|
+
]);
|
|
35
|
+
/** Commands that break the line: the title goes on as a second part, a subtitle. */
|
|
36
|
+
const LINE_BREAKS = new Set([
|
|
37
|
+
"\\\\",
|
|
38
|
+
"newline",
|
|
39
|
+
"linebreak",
|
|
40
|
+
]);
|
|
41
|
+
/** Commands that put nothing in the title's words: a label, a footnote, an index entry. */
|
|
42
|
+
const INVISIBLE = new Set([
|
|
43
|
+
"label",
|
|
44
|
+
"footnote",
|
|
45
|
+
"thanks",
|
|
46
|
+
"index",
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* The command a macro is, for the case judge: the paper's `\title`, a heading at its level, or none.
|
|
50
|
+
* `\title` is read wherever it stands — in the preamble, where acmart and IEEEtran put it, or in a
|
|
51
|
+
* file the paper includes.
|
|
52
|
+
*/
|
|
53
|
+
const commandOf = (m) => {
|
|
54
|
+
if (m.content === "title")
|
|
55
|
+
return [{ kind: "title" }];
|
|
56
|
+
const level = LEVELS.get(m.content);
|
|
57
|
+
return level === undefined ? [] : [{ kind: "heading", level }];
|
|
58
|
+
};
|
|
59
|
+
/** A macro that is neither left out of the title nor read as the words it wraps. */
|
|
60
|
+
const isOpaqueMacro = (m) => !INVISIBLE.has(m.content) &&
|
|
61
|
+
!TRANSPARENT.has(m.content) &&
|
|
62
|
+
!LINE_BREAKS.has(m.content);
|
|
63
|
+
/** What one macro says in a title. */
|
|
64
|
+
function macroSegments(m) {
|
|
65
|
+
if (INVISIBLE.has(m.content))
|
|
66
|
+
return [];
|
|
67
|
+
if (LINE_BREAKS.has(m.content))
|
|
68
|
+
return textAt(m, LINE_BREAK);
|
|
69
|
+
if (TRANSPARENT.has(m.content))
|
|
70
|
+
return segmentsOf(mandatory(m).at(-1)?.content);
|
|
71
|
+
return textAt(m, OPAQUE);
|
|
72
|
+
}
|
|
73
|
+
/** A piece of text at the start of a node. */
|
|
74
|
+
const textAt = (n, text) => inPlace(placeOf(n), (s) => [{ text, at: s.start }]);
|
|
75
|
+
/** What one node says in a title: text as itself, a formatting command's words, anything else opaque. */
|
|
76
|
+
function nodeSegments(n) {
|
|
77
|
+
if (n.type === "string")
|
|
78
|
+
return textAt(n, stringText(n.content));
|
|
79
|
+
if (n.type === "whitespace" || n.type === "parbreak")
|
|
80
|
+
return textAt(n, " ");
|
|
81
|
+
// A comment ends its line: the words on either side of it are two words.
|
|
82
|
+
if (n.type === "comment")
|
|
83
|
+
return textAt(n, " ");
|
|
84
|
+
if (n.type === "group")
|
|
85
|
+
return segmentsOf(n.content);
|
|
86
|
+
if (n.type === "macro")
|
|
87
|
+
return macroSegments(n);
|
|
88
|
+
return textAt(n, OPAQUE);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A group that is the very next node after a command the parser has no signature for (`\ours{…}`, a macro of the
|
|
92
|
+
* author's own): the parser leaves such a group beside the macro, but TeX hands it to the macro as an
|
|
93
|
+
* argument, so it is part of what the macro prints and not words of the title. A chain of groups
|
|
94
|
+
* (`\findingdes{label}{a sentence}`) is all argument.
|
|
95
|
+
*/
|
|
96
|
+
function isArgumentGroup(nodes, n, i) {
|
|
97
|
+
const prev = nodes[i - 1];
|
|
98
|
+
if (prev === undefined || n.type !== "group")
|
|
99
|
+
return false;
|
|
100
|
+
return prev.type === "macro"
|
|
101
|
+
? isOpaqueMacro(prev)
|
|
102
|
+
: isArgumentGroup(nodes, prev, i - 1);
|
|
103
|
+
}
|
|
104
|
+
const segmentsOf = (nodes) => {
|
|
105
|
+
const list = nodes ?? [];
|
|
106
|
+
return list.flatMap((n, i) => isArgumentGroup(list, n, i) ? [] : nodeSegments(n));
|
|
107
|
+
};
|
|
108
|
+
/** One argument's contents → the heading it makes, or none when it says nothing. */
|
|
109
|
+
function titled(command, argument, nodes) {
|
|
110
|
+
const segments = segmentsOf(nodes);
|
|
111
|
+
return hasAtLeast(segments, 1)
|
|
112
|
+
? [{ command, argument, title: { segments } }]
|
|
113
|
+
: [];
|
|
114
|
+
}
|
|
115
|
+
/** One titled command → the short title (`[…]`) and the title (the last `{…}`), each judged. */
|
|
116
|
+
const headingsOfMacro = (m) => commandOf(m).flatMap((command) => [
|
|
117
|
+
...titled(command, "short", optional(m)?.content),
|
|
118
|
+
...titled(command, "title", mandatory(m).at(-1)?.content),
|
|
119
|
+
]);
|
|
120
|
+
/**
|
|
121
|
+
* The paper's `\title` and every `\section`, `\subsection`, `\subsubsection`, `\paragraph` and
|
|
122
|
+
* `\subparagraph`, starred or not, each with its optional short title, in document order. What the
|
|
123
|
+
* PDF never shows is pruned (`isUnrendered`), and so is a macro definition's body: a heading in a
|
|
124
|
+
* comment environment or in a definition is not a heading where it is written.
|
|
125
|
+
*/
|
|
126
|
+
export const headingsOf = (t) => visited(t.root, (v, parent) => isUnrendered(v, parent) || isDefinition(v))
|
|
127
|
+
.filter(isNode)
|
|
128
|
+
.flatMap((n) => (n.type === "macro" ? headingsOfMacro(n) : []));
|
|
129
|
+
//# sourceMappingURL=headings.js.map
|