paperlint 4.2.0 → 6.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/CLAUDE.md +15 -2
- package/CONTRIBUTING.md +3 -0
- package/README.md +23 -12
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/bin/paperlint.mjs +5 -1
- package/dist/adapters/latex/includes.d.ts +5 -1
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +8 -2
- package/dist/adapters/latex/includes.js.map +1 -1
- 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/layout.d.ts +27 -0
- package/dist/adapters/latex/layout.d.ts.map +1 -0
- package/dist/adapters/latex/layout.js +62 -0
- package/dist/adapters/latex/layout.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +11 -1
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +5 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -42
- package/dist/cli.js.map +1 -1
- package/dist/domain/anonymity.d.ts +68 -0
- package/dist/domain/anonymity.d.ts.map +1 -0
- package/dist/domain/anonymity.js +94 -0
- package/dist/domain/anonymity.js.map +1 -0
- package/dist/domain/body-pages.d.ts +69 -0
- package/dist/domain/body-pages.d.ts.map +1 -0
- package/dist/domain/body-pages.js +141 -0
- package/dist/domain/body-pages.js.map +1 -0
- package/dist/domain/paper-source.js +2 -2
- package/dist/domain/tex-document.d.ts +9 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts +5 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +6 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts +20 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -0
- package/dist/eslint-rules/paper-context.js +44 -0
- package/dist/eslint-rules/paper-context.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +1 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +32 -18
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +7 -1
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts +28 -2
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +54 -9
- package/dist/facts-file.js.map +1 -1
- package/dist/init.d.ts +4 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +24 -29
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/package-dirs.d.ts +2 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +8 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +2 -2
- package/dist/paper-includes.d.ts +17 -0
- package/dist/paper-includes.d.ts.map +1 -0
- package/dist/paper-includes.js +51 -0
- package/dist/paper-includes.js.map +1 -0
- package/dist/paper-settings.d.ts +11 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +38 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-content.d.ts +40 -0
- package/dist/pdf-content.d.ts.map +1 -0
- package/dist/pdf-content.js +97 -0
- package/dist/pdf-content.js.map +1 -0
- package/dist/pdf-facts.d.ts +30 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +37 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/ports/latex.d.ts +3 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -0
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.d.ts +1 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +17 -3
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +36 -38
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +40 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +38 -8
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +179 -41
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +23 -20
- package/docs/e2e.md +32 -11
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/anonymity/identity.md +111 -0
- package/docs/rules/format/layout-override.md +61 -0
- package/docs/rules/format/page-limit.md +108 -0
- package/docs/rules/pdf/body-size.md +87 -0
- package/docs/rules.md +55 -46
- package/eslint-rules/latex-language.ts +10 -4
- package/eslint-rules/paper-context.test.ts +47 -0
- package/eslint-rules/paper-context.ts +57 -0
- package/eslint-rules/paper-typography.ts +67 -23
- package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
- package/eslint-rules/pdf-last-page-balance.ts +2 -2
- package/eslint-rules/tex-build.ts +8 -1
- package/fixtures/build-e2e/aidc/expect.json +1 -1
- package/fixtures/build-e2e/aidc/paperlint.json +1 -1
- package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-ack/expect.json +19 -0
- package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
- package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-author/expect.json +19 -0
- package/fixtures/build-e2e/anon-author/paper.tex +29 -0
- package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-bib/expect.json +20 -0
- package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
- package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-href/expect.json +19 -0
- package/fixtures/build-e2e/anon-href/paper.tex +30 -0
- package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-macro/expect.json +19 -0
- package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
- package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
- package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
- package/fixtures/build-e2e/unbalanced/expect.json +2 -2
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/lib/paper-config.ts +3 -1
- package/package.json +1 -1
- package/presets/acm-sigconf.jsonc +3 -1
- package/presets/aidc.jsonc +16 -17
- package/presets/ieee-conference.jsonc +8 -0
- package/presets/venue-profile.schema.json +9 -0
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
- package/src/adapters/latex/includes.ts +10 -2
- package/src/adapters/latex/index.test.ts +8 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/layout.ts +106 -0
- package/src/adapters/latex/nodes.ts +11 -3
- package/src/adapters/latex/parse.ts +5 -1
- package/src/build.harness.mjs +6 -4
- package/src/build.test.ts +23 -1
- package/src/build.ts +7 -3
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +15 -18
- package/src/cli.ts +114 -46
- package/src/domain/anonymity.test.ts +163 -0
- package/src/domain/anonymity.ts +129 -0
- package/src/domain/body-pages.test.ts +300 -0
- package/src/domain/body-pages.ts +223 -0
- package/src/domain/paper-source.ts +2 -2
- package/src/domain/tex-document.ts +10 -0
- package/src/facts-file.test.ts +100 -4
- package/src/facts-file.ts +91 -9
- package/src/init.test.ts +33 -13
- package/src/init.ts +25 -48
- package/src/linted-set.test.ts +11 -1
- package/src/new-paper.harness.mjs +1 -5
- package/src/new-paper.test.ts +7 -1
- package/src/package-dirs.ts +9 -0
- package/src/paper-files.ts +2 -2
- package/src/paper-includes.test.ts +161 -0
- package/src/paper-includes.ts +77 -0
- package/src/paper-settings.test.ts +63 -6
- package/src/paper-settings.ts +53 -5
- package/src/pdf-content.test.ts +71 -0
- package/src/pdf-content.ts +125 -0
- package/src/pdf-facts.test.ts +110 -2
- package/src/pdf-facts.ts +106 -8
- package/src/ports/latex.ts +3 -0
- package/src/presets.ts +8 -8
- package/src/rule-cases.test.ts +29 -33
- package/src/rule-docs.test.ts +0 -3
- package/src/rules-config.ts +1 -0
- package/src/structure.harness.ts +1 -3
- package/src/structure.ts +1 -1
- package/src/tex-paper.test.ts +78 -61
- package/src/tex-paper.ts +57 -48
- package/src/tex-requirements.ts +9 -0
- package/src/tex-venue-rules.test.ts +111 -6
- package/src/tex-venue-rules.ts +57 -2
- package/src/types.ts +2 -0
- package/src/venue-rules.test.ts +351 -39
- package/src/venue-rules.ts +247 -47
- package/templates/paper/PIPELINE-STATUS.md +0 -4
- package/dist/action-ref.d.ts +0 -12
- package/dist/action-ref.d.ts.map +0 -1
- package/dist/action-ref.js +0 -16
- package/dist/action-ref.js.map +0 -1
- package/dist/eslint-rules/paper-research-question.d.ts +0 -31
- package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
- package/dist/eslint-rules/paper-research-question.js +0 -188
- package/dist/eslint-rules/paper-research-question.js.map +0 -1
- package/docs/rules/tex/missing-input.md +0 -58
- package/eslint-rules/paper-research-question.harness.mjs +0 -212
- package/eslint-rules/paper-research-question.ts +0 -199
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
- package/fixtures/paper-research-question/draft/paper.tex +0 -2
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FILES OF A PAPER'S BODY, handed to ESLint — so the rules over ESLint's LaTeX text read a paper
|
|
3
|
+
* whose body is in `sections/*.tex`, and report each finding at its own file and line (#144).
|
|
4
|
+
*
|
|
5
|
+
* The set is what TeX reads, resolved by the reader the parse-tree rules use (`bodyFiles`,
|
|
6
|
+
* src/tex-paper.ts): `paper.tex` and every file an include in its document body brings in, found in
|
|
7
|
+
* the paper's directory. Not a glob over every `.tex`: a frozen version under `versions/`, a macro
|
|
8
|
+
* file the preamble includes and a scratch file nothing includes are not the paper.
|
|
9
|
+
*
|
|
10
|
+
* Two blocks per paper, both relative to the paper's directory (`basePath`):
|
|
11
|
+
*
|
|
12
|
+
* 1. a global un-ignore of each body file by name. paperlint's scope ignores every file it does not
|
|
13
|
+
* claim (src/paper-files.ts); the fragment block (`buildConfig`) claims every `.tex` but
|
|
14
|
+
* `paper.tex`, so only the files named here are ever enumerated;
|
|
15
|
+
* 2. `settings.paperlint.paper` for `paper.tex` and its body files: the main file and the whole set,
|
|
16
|
+
* for the rules whose verdict is the paper's (eslint-rules/paper-context.ts).
|
|
17
|
+
*/
|
|
18
|
+
import { join, relative } from "node:path";
|
|
19
|
+
import { PAPERLINT_SETTINGS } from "#eslint-rules/paper-context";
|
|
20
|
+
import { callerPath } from "./caller-path.ts";
|
|
21
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
22
|
+
import type { ConfigBlock } from "./rules-config.ts";
|
|
23
|
+
import { bodyFiles, type PaperDeps, type Unread } from "./tex-paper.ts";
|
|
24
|
+
|
|
25
|
+
/** One paper's main file and body files, and the includes that resolved nowhere. */
|
|
26
|
+
export interface PaperBody {
|
|
27
|
+
readonly dir: string;
|
|
28
|
+
readonly main: AbsolutePath;
|
|
29
|
+
readonly files: readonly AbsolutePath[];
|
|
30
|
+
readonly missing: readonly Unread[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The body of each paper directory in `dirs` that has a `paper.tex`. */
|
|
34
|
+
export function paperBodies(
|
|
35
|
+
dirs: readonly string[],
|
|
36
|
+
deps: PaperDeps,
|
|
37
|
+
): readonly PaperBody[] {
|
|
38
|
+
return dirs.flatMap((dir) => {
|
|
39
|
+
const main = callerPath(join(dir, "paper.tex"));
|
|
40
|
+
const bytes = deps.files.readBytes(main);
|
|
41
|
+
if (bytes === null) return [];
|
|
42
|
+
const body = bodyFiles(main, new TextDecoder().decode(bytes), deps);
|
|
43
|
+
return [{ dir, main, files: body.files, missing: body.missing }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The config blocks that hand ESLint each paper's body files, and the paper to every one of them. */
|
|
48
|
+
export const includeBlocks = (
|
|
49
|
+
bodies: readonly PaperBody[],
|
|
50
|
+
): readonly ConfigBlock[] =>
|
|
51
|
+
bodies.flatMap((b) => {
|
|
52
|
+
const rel = b.files.map((f) => relative(b.dir, f));
|
|
53
|
+
const paper = { main: b.main, files: [b.main, ...b.files] };
|
|
54
|
+
return [
|
|
55
|
+
...(rel.length > 0
|
|
56
|
+
? [{ basePath: b.dir, ignores: rel.map((r) => `!${r}`) }]
|
|
57
|
+
: []),
|
|
58
|
+
{
|
|
59
|
+
basePath: b.dir,
|
|
60
|
+
files: ["paper.tex", ...rel],
|
|
61
|
+
settings: { [PAPERLINT_SETTINGS]: { paper } },
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** One line per include that resolved nowhere: which text lint did not read, and why. */
|
|
67
|
+
export const unreadLines = (
|
|
68
|
+
bodies: readonly PaperBody[],
|
|
69
|
+
shown: (path: string) => string,
|
|
70
|
+
): readonly string[] =>
|
|
71
|
+
bodies.flatMap((b) =>
|
|
72
|
+
b.missing.map(
|
|
73
|
+
(m) =>
|
|
74
|
+
`${shown(b.main)}: lint did not read \`${m.target}\` (included by ${m.file}): ` +
|
|
75
|
+
"no such file in the paper's directory or paperlint's inputs",
|
|
76
|
+
),
|
|
77
|
+
);
|
|
@@ -30,18 +30,31 @@ describe("parsePaperSettings", () => {
|
|
|
30
30
|
kind: "research",
|
|
31
31
|
pdf: null,
|
|
32
32
|
rules: null,
|
|
33
|
+
identity: null,
|
|
33
34
|
},
|
|
34
35
|
});
|
|
35
36
|
expect(parsePaperSettings({})).toEqual({
|
|
36
37
|
ok: true,
|
|
37
|
-
value: {
|
|
38
|
+
value: {
|
|
39
|
+
extends: null,
|
|
40
|
+
kind: null,
|
|
41
|
+
pdf: null,
|
|
42
|
+
rules: null,
|
|
43
|
+
identity: null,
|
|
44
|
+
},
|
|
38
45
|
});
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
it("extends: null is valid and means no venue chosen yet — what `paperlint new` writes", () => {
|
|
42
49
|
expect(parsePaperSettings({ extends: null })).toEqual({
|
|
43
50
|
ok: true,
|
|
44
|
-
value: {
|
|
51
|
+
value: {
|
|
52
|
+
extends: null,
|
|
53
|
+
kind: null,
|
|
54
|
+
pdf: null,
|
|
55
|
+
rules: null,
|
|
56
|
+
identity: null,
|
|
57
|
+
},
|
|
45
58
|
});
|
|
46
59
|
});
|
|
47
60
|
});
|
|
@@ -138,6 +151,9 @@ describe("readPaperSettings — a root paperlint.json that cannot be used", () =
|
|
|
138
151
|
});
|
|
139
152
|
});
|
|
140
153
|
|
|
154
|
+
/** The fields a case does not set. */
|
|
155
|
+
const UNSET = { pdf: null, rules: null, identity: null };
|
|
156
|
+
|
|
141
157
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
142
158
|
const ROOT = "/work";
|
|
143
159
|
const withFiles = (f: Record<string, string>) =>
|
|
@@ -161,8 +177,7 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
161
177
|
).toEqual({
|
|
162
178
|
extends: "paperlint:agenticdev",
|
|
163
179
|
kind: "short",
|
|
164
|
-
|
|
165
|
-
rules: null,
|
|
180
|
+
...UNSET,
|
|
166
181
|
});
|
|
167
182
|
});
|
|
168
183
|
|
|
@@ -172,8 +187,7 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
172
187
|
).toEqual({
|
|
173
188
|
extends: "paperlint:aisec",
|
|
174
189
|
kind: null,
|
|
175
|
-
|
|
176
|
-
rules: null,
|
|
190
|
+
...UNSET,
|
|
177
191
|
});
|
|
178
192
|
});
|
|
179
193
|
|
|
@@ -191,6 +205,7 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
191
205
|
pdf: null,
|
|
192
206
|
// The root's rules are the project's blocks (cli.ts), not this paper's.
|
|
193
207
|
rules: { "pdf/fonts": "off" },
|
|
208
|
+
identity: null,
|
|
194
209
|
});
|
|
195
210
|
});
|
|
196
211
|
|
|
@@ -232,6 +247,7 @@ describe("paperRules — `rules` in paperlint.json", () => {
|
|
|
232
247
|
kind: null,
|
|
233
248
|
pdf: null,
|
|
234
249
|
rules,
|
|
250
|
+
identity: null,
|
|
235
251
|
});
|
|
236
252
|
|
|
237
253
|
it("no rules: none", () => {
|
|
@@ -267,3 +283,44 @@ describe("paperRules — `rules` in paperlint.json", () => {
|
|
|
267
283
|
}
|
|
268
284
|
});
|
|
269
285
|
});
|
|
286
|
+
|
|
287
|
+
describe("readPaperSettings — identity, at both levels", () => {
|
|
288
|
+
const ROOT = "/work";
|
|
289
|
+
const withFiles = (f: Record<string, string>) =>
|
|
290
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
291
|
+
const read = (f: Record<string, string>) => {
|
|
292
|
+
const r = readPaperSettings(withFiles(f), PAPER);
|
|
293
|
+
if (!r.ok) throw new Error(r.error.why);
|
|
294
|
+
return r.value;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
it("identity: the root's list and the paper's, joined without repeats", () => {
|
|
298
|
+
expect(
|
|
299
|
+
read({
|
|
300
|
+
[`${ROOT}/paperlint.json`]: '{"identity":["Ada Example","adaexample"]}',
|
|
301
|
+
[`${PAPER}/paperlint.json`]:
|
|
302
|
+
'{"identity":["adaexample","Bob Coauthor"]}',
|
|
303
|
+
})?.identity,
|
|
304
|
+
).toEqual(["Ada Example", "adaexample", "Bob Coauthor"]);
|
|
305
|
+
expect(
|
|
306
|
+
read({ [`${ROOT}/paperlint.json`]: '{"identity":["Ada Example"]}' })
|
|
307
|
+
?.identity,
|
|
308
|
+
).toEqual(["Ada Example"]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("identity of the wrong shape, at either level, is refused naming it", () => {
|
|
312
|
+
const why = (f: Record<string, string>) => {
|
|
313
|
+
const r = readPaperSettings(withFiles(f), PAPER);
|
|
314
|
+
return r.ok ? "" : r.error.why;
|
|
315
|
+
};
|
|
316
|
+
expect(why({ [`${PAPER}/paperlint.json`]: '{"identity":"Ada"}' })).toMatch(
|
|
317
|
+
/"identity" must be a list of strings/,
|
|
318
|
+
);
|
|
319
|
+
expect(why({ [`${PAPER}/paperlint.json`]: '{"identity":[" "]}' })).toMatch(
|
|
320
|
+
/"identity" must be a list of strings, each with a letter or digit/,
|
|
321
|
+
);
|
|
322
|
+
expect(why({ [`${ROOT}/paperlint.json`]: '{"identity":[3]}' })).toMatch(
|
|
323
|
+
/^the root paperlint\.json: "identity" must be/,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
});
|
package/src/paper-settings.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface PaperSettings {
|
|
|
45
45
|
* against the shipped rules.
|
|
46
46
|
*/
|
|
47
47
|
readonly rules: Readonly<Record<string, unknown>> | readonly unknown[] | null;
|
|
48
|
+
/**
|
|
49
|
+
* What identifies the authors — `anonymity/identity` requires a blind venue's PDF to say none of it.
|
|
50
|
+
* The root's list and the paper's own, joined: a paper adds its co-authors to the project's author
|
|
51
|
+
* and cannot drop them by accident. Null when neither declares one.
|
|
52
|
+
*/
|
|
53
|
+
readonly identity: readonly string[] | null;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/** Why a paper's settings cannot be read. */
|
|
@@ -86,6 +92,23 @@ export function stringFields(
|
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `identity`: absent is null; otherwise a list of strings, each with a letter or digit in it — a
|
|
97
|
+
* blank entry would match nothing and read as a declared, clean identity.
|
|
98
|
+
*/
|
|
99
|
+
export function identityField(
|
|
100
|
+
d: Readonly<Record<string, unknown>>,
|
|
101
|
+
): Result<readonly string[] | null, string> {
|
|
102
|
+
const v = d["identity"];
|
|
103
|
+
if (v === undefined) return ok(null);
|
|
104
|
+
return Array.isArray(v) &&
|
|
105
|
+
v.every((x) => typeof x === "string" && /[\p{L}\p{N}]/u.test(x))
|
|
106
|
+
? ok(v.filter((x): x is string => typeof x === "string"))
|
|
107
|
+
: err(
|
|
108
|
+
`"identity" must be a list of strings, each with a letter or digit, got ${JSON.stringify(v)}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
/** The parsed JSON of `paperlint.json` → the settings, or one line saying what is wrong. Pure. */
|
|
90
113
|
export function parsePaperSettings(
|
|
91
114
|
json: unknown,
|
|
@@ -103,6 +126,8 @@ export function parsePaperSettings(
|
|
|
103
126
|
);
|
|
104
127
|
const fields = stringFields(json);
|
|
105
128
|
if (!fields.ok) return fields;
|
|
129
|
+
const identity = identityField(json);
|
|
130
|
+
if (!identity.ok) return identity;
|
|
106
131
|
const rules = json["rules"];
|
|
107
132
|
if (rules !== undefined && !isObject(rules) && !Array.isArray(rules))
|
|
108
133
|
return err(
|
|
@@ -111,6 +136,7 @@ export function parsePaperSettings(
|
|
|
111
136
|
return ok({
|
|
112
137
|
...fields.value,
|
|
113
138
|
rules: rules ?? null,
|
|
139
|
+
identity: identity.value,
|
|
114
140
|
});
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -144,9 +170,18 @@ function rootDefaults(
|
|
|
144
170
|
if (!json.ok) return json;
|
|
145
171
|
if (!isObject(json.value)) return ok(null);
|
|
146
172
|
const fields = stringFields(json.value);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
const identity = identityField(json.value);
|
|
174
|
+
if (!fields.ok)
|
|
175
|
+
return err({
|
|
176
|
+
kind: "broken",
|
|
177
|
+
why: `the root ${CONFIG_FILE}: ${fields.error}`,
|
|
178
|
+
});
|
|
179
|
+
return identity.ok
|
|
180
|
+
? ok({ ...fields.value, identity: identity.value })
|
|
181
|
+
: err({
|
|
182
|
+
kind: "broken",
|
|
183
|
+
why: `the root ${CONFIG_FILE}: ${identity.error}`,
|
|
184
|
+
});
|
|
150
185
|
}
|
|
151
186
|
|
|
152
187
|
/**
|
|
@@ -167,8 +202,20 @@ export function readPaperSettings(
|
|
|
167
202
|
return ok(merge(defaults.value, parsed.value));
|
|
168
203
|
}
|
|
169
204
|
|
|
170
|
-
type Defaults = Pick<PaperSettings, "extends" | "kind" | "pdf">;
|
|
171
|
-
const NO_DEFAULTS: Defaults = {
|
|
205
|
+
type Defaults = Pick<PaperSettings, "extends" | "kind" | "pdf" | "identity">;
|
|
206
|
+
const NO_DEFAULTS: Defaults = {
|
|
207
|
+
extends: null,
|
|
208
|
+
kind: null,
|
|
209
|
+
pdf: null,
|
|
210
|
+
identity: null,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Both lists, joined without repeats; null when neither side has one. */
|
|
214
|
+
const joined = (
|
|
215
|
+
a: readonly string[] | null,
|
|
216
|
+
b: readonly string[] | null,
|
|
217
|
+
): readonly string[] | null =>
|
|
218
|
+
a === null && b === null ? null : [...new Set([...(a ?? []), ...(b ?? [])])];
|
|
172
219
|
|
|
173
220
|
/** The paper's own values win; each absent one falls back to the root's. */
|
|
174
221
|
function merge(
|
|
@@ -182,6 +229,7 @@ function merge(
|
|
|
182
229
|
kind: p.kind ?? r.kind,
|
|
183
230
|
pdf: p.pdf ?? r.pdf,
|
|
184
231
|
rules: p.rules,
|
|
232
|
+
identity: joined(r.identity, p.identity),
|
|
185
233
|
};
|
|
186
234
|
return paper === null && Object.values(merged).every((v) => v === null)
|
|
187
235
|
? null
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** What a page says, read from pdf.js's shapes: text, links, metadata. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { linkTargetsOf, metadataOf, pageTextOf, xmpOf } from "./pdf-content.ts";
|
|
4
|
+
|
|
5
|
+
describe("pageTextOf", () => {
|
|
6
|
+
it("joins items without a space, and breaks the line where pdf.js marks one", () => {
|
|
7
|
+
expect(
|
|
8
|
+
pageTextOf([
|
|
9
|
+
{ str: "Ada Ex", hasEOL: false },
|
|
10
|
+
{ str: "ample", hasEOL: true },
|
|
11
|
+
{ type: "beginMarkedContent" },
|
|
12
|
+
{ str: "", hasEOL: true },
|
|
13
|
+
{ str: "Intro" },
|
|
14
|
+
]),
|
|
15
|
+
).toBe("Ada Example\n\nIntro");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("linkTargetsOf", () => {
|
|
20
|
+
it("each link's target once: url, else unsafeUrl; not other annotations, not empty ones", () => {
|
|
21
|
+
expect(
|
|
22
|
+
linkTargetsOf([
|
|
23
|
+
{ subtype: "Link", url: "https://a.example" },
|
|
24
|
+
{ subtype: "Link", url: "https://a.example" },
|
|
25
|
+
{ subtype: "Link", unsafeUrl: "mailto:x@example.org" },
|
|
26
|
+
{ subtype: "Link", dest: "sec:1" },
|
|
27
|
+
{ subtype: "Link", url: "" },
|
|
28
|
+
{ subtype: "Widget", url: "https://b.example" },
|
|
29
|
+
]),
|
|
30
|
+
).toEqual(["https://a.example", "mailto:x@example.org"]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("metadataOf and xmpOf", () => {
|
|
35
|
+
it("every string of the Info dictionary, its custom entries and the XMP packet, flat", () => {
|
|
36
|
+
const xmp = xmpOf(
|
|
37
|
+
new Map<unknown, unknown>([
|
|
38
|
+
["dc:creator", ["Ada Example", "Bob Other"]],
|
|
39
|
+
["dc:title", "A Title"],
|
|
40
|
+
["xmp:rating", 3],
|
|
41
|
+
[7, "not a name"],
|
|
42
|
+
]),
|
|
43
|
+
);
|
|
44
|
+
expect(xmp).toEqual({
|
|
45
|
+
"dc:creator": ["Ada Example", "Bob Other"],
|
|
46
|
+
"dc:title": "A Title",
|
|
47
|
+
"xmp:rating": 3,
|
|
48
|
+
});
|
|
49
|
+
expect(
|
|
50
|
+
metadataOf(
|
|
51
|
+
{
|
|
52
|
+
Author: "Ada Example",
|
|
53
|
+
Subject: "",
|
|
54
|
+
Trapped: { name: "False" },
|
|
55
|
+
Custom: { "PTEX.Fullbanner": "This is pdfTeX" },
|
|
56
|
+
},
|
|
57
|
+
xmp,
|
|
58
|
+
),
|
|
59
|
+
).toEqual({
|
|
60
|
+
Author: "Ada Example",
|
|
61
|
+
"PTEX.Fullbanner": "This is pdfTeX",
|
|
62
|
+
"dc:creator": "Ada Example; Bob Other",
|
|
63
|
+
"dc:title": "A Title",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("no Info and no packet: nothing", () => {
|
|
68
|
+
expect(xmpOf(null)).toBeNull();
|
|
69
|
+
expect(metadataOf(null, null)).toEqual({});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a page SAYS, as pdf.js exports it — PURE. The anonymity and page-limit rules judge text, so
|
|
3
|
+
* the facts carry each page's text, the targets of its links, where the bibliography's first entry
|
|
4
|
+
* and the appendix are anchored, and the document's metadata. `pdf-facts.ts` reads the PDF and
|
|
5
|
+
* calls these; nothing here knows what a rule does with the result.
|
|
6
|
+
*/
|
|
7
|
+
import { fieldOf, isRecord } from "./domain/record.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A page's text: its items in content order, joined WITHOUT a separator — pdf.js emits a space as
|
|
11
|
+
* an item of its own where the glyphs leave a gap, so a word split into two items by kerning comes
|
|
12
|
+
* back whole — with a line break after every item pdf.js marks as ending a line.
|
|
13
|
+
*/
|
|
14
|
+
export const pageTextOf = (items: readonly unknown[]): string =>
|
|
15
|
+
items
|
|
16
|
+
.map((it) => {
|
|
17
|
+
const s = fieldOf(it, "str");
|
|
18
|
+
return `${typeof s === "string" ? s : ""}${fieldOf(it, "hasEOL") === true ? "\n" : ""}`;
|
|
19
|
+
})
|
|
20
|
+
.join("");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The target of every link annotation on a page, once each: its `url`, else its `unsafeUrl`. A link
|
|
24
|
+
* whose text breaks across lines is one annotation per line (measured: hyperref's `\\href` gave two
|
|
25
|
+
* for one target), and one target is one fact.
|
|
26
|
+
*/
|
|
27
|
+
export const linkTargetsOf = (
|
|
28
|
+
annotations: readonly unknown[],
|
|
29
|
+
): readonly string[] => {
|
|
30
|
+
const urls = annotations.flatMap((a) => {
|
|
31
|
+
if (fieldOf(a, "subtype") !== "Link") return [];
|
|
32
|
+
const url = fieldOf(a, "url") ?? fieldOf(a, "unsafeUrl");
|
|
33
|
+
return typeof url === "string" && url !== "" ? [url] : [];
|
|
34
|
+
});
|
|
35
|
+
return urls.filter((u, i) => urls.indexOf(u) === i);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The page reference an explicit destination starts with, or null for anything else. */
|
|
39
|
+
export const refOf = (dest: unknown): object | null => {
|
|
40
|
+
const ref: unknown = Array.isArray(dest) ? dest[0] : undefined;
|
|
41
|
+
return typeof ref === "object" && ref !== null ? ref : null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The page references of the named destinations whose name starts with `prefix`, from pdf.js's
|
|
46
|
+
* `getDestinations()` (name → explicit destination). hyperref names one `cite.<key>` per
|
|
47
|
+
* `\bibitem`, and, after `\appendix` in the standard classes, the appendix's sections
|
|
48
|
+
* `appendix.<letter>`.
|
|
49
|
+
*/
|
|
50
|
+
export const namedRefs = (
|
|
51
|
+
destinations: unknown,
|
|
52
|
+
prefix: string,
|
|
53
|
+
): readonly object[] =>
|
|
54
|
+
isRecord(destinations)
|
|
55
|
+
? Object.entries(destinations).flatMap(([name, dest]) => {
|
|
56
|
+
const ref = refOf(dest);
|
|
57
|
+
return name.startsWith(prefix) && ref !== null ? [ref] : [];
|
|
58
|
+
})
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
/** A bookmark title that opens an appendix: «Appendix A: …», «APPENDIX», «Appendices». */
|
|
62
|
+
const APPENDIX_TITLE = /^\s*appendi(?:x|ces)\b/iu;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The destinations of the top-level bookmarks that open an appendix, from pdf.js's `getOutline()`:
|
|
66
|
+
* each a name to look up, or an explicit destination. IEEEtran's `\appendices` names its sections
|
|
67
|
+
* `section*.<n>`, so the bookmark's title is what says it is an appendix.
|
|
68
|
+
*/
|
|
69
|
+
export const appendixOutlineDests = (outline: unknown): readonly unknown[] =>
|
|
70
|
+
Array.isArray(outline)
|
|
71
|
+
? outline.flatMap((item: unknown) => {
|
|
72
|
+
const title = fieldOf(item, "title");
|
|
73
|
+
return typeof title === "string" && APPENDIX_TITLE.test(title)
|
|
74
|
+
? [fieldOf(item, "dest")]
|
|
75
|
+
: [];
|
|
76
|
+
})
|
|
77
|
+
: [];
|
|
78
|
+
|
|
79
|
+
/** A value of a metadata field as text: a string, or the strings of a list; nothing else. */
|
|
80
|
+
const textsOf = (v: unknown): readonly string[] =>
|
|
81
|
+
typeof v === "string"
|
|
82
|
+
? [v]
|
|
83
|
+
: Array.isArray(v)
|
|
84
|
+
? v.filter((x): x is string => typeof x === "string")
|
|
85
|
+
: [];
|
|
86
|
+
|
|
87
|
+
const entries = (rec: unknown): readonly (readonly [string, string])[] =>
|
|
88
|
+
isRecord(rec)
|
|
89
|
+
? Object.entries(rec).flatMap(([k, v]) => {
|
|
90
|
+
const text = textsOf(v).join("; ");
|
|
91
|
+
return text === "" ? [] : [[k, text] as const];
|
|
92
|
+
})
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The XMP packet pdf.js parsed, as `name → value`: its `Metadata` iterates `[name, value]` pairs.
|
|
97
|
+
* Null when the PDF carries no packet.
|
|
98
|
+
*/
|
|
99
|
+
export const xmpOf = (
|
|
100
|
+
packet: Iterable<unknown> | null,
|
|
101
|
+
): Readonly<Record<string, unknown>> | null =>
|
|
102
|
+
packet === null
|
|
103
|
+
? null
|
|
104
|
+
: Object.fromEntries(
|
|
105
|
+
[...packet].flatMap((e) =>
|
|
106
|
+
Array.isArray(e) && typeof e[0] === "string" ? [[e[0], e[1]]] : [],
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The document's metadata as flat `field → text`: every string of the Info dictionary (`Author`,
|
|
112
|
+
* `Title`, …), of its custom entries (`PTEX.Fullbanner`), and of the XMP packet, whose names carry
|
|
113
|
+
* their namespace (`dc:creator`), as pdf.js's `getMetadata` returns them. `xmp` is the packet's
|
|
114
|
+
* `xmpOf`, or null when the PDF has none.
|
|
115
|
+
*/
|
|
116
|
+
export function metadataOf(
|
|
117
|
+
info: unknown,
|
|
118
|
+
xmp: unknown,
|
|
119
|
+
): Readonly<Record<string, string>> {
|
|
120
|
+
return Object.fromEntries([
|
|
121
|
+
...entries(info),
|
|
122
|
+
...entries(fieldOf(info, "Custom")),
|
|
123
|
+
...entries(xmp),
|
|
124
|
+
]);
|
|
125
|
+
}
|
package/src/pdf-facts.test.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
8
|
import { getResolvedPDFJS } from "unpdf";
|
|
9
|
+
import { fieldOf } from "./domain/record.ts";
|
|
9
10
|
import { test } from "vitest";
|
|
10
11
|
import { factsOf, failureOf, rawFontOf } from "./pdf-facts.ts";
|
|
11
12
|
import { present } from "../test/support.ts";
|
|
@@ -21,7 +22,11 @@ interface FakeItem {
|
|
|
21
22
|
height: number;
|
|
22
23
|
}
|
|
23
24
|
/** A page whose text items and loaded fonts are given; its viewport flips y like pdf.js's. */
|
|
24
|
-
function page(
|
|
25
|
+
function page(
|
|
26
|
+
items: FakeItem[],
|
|
27
|
+
fonts: Record<string, unknown>,
|
|
28
|
+
said: { annotations?: unknown[] } = {},
|
|
29
|
+
) {
|
|
25
30
|
const objs = new Map(Object.entries(fonts));
|
|
26
31
|
return {
|
|
27
32
|
getOperatorList: () => Promise.resolve({ fnArray: [], argsArray: [] }),
|
|
@@ -32,6 +37,7 @@ function page(items: FakeItem[], fonts: Record<string, unknown>) {
|
|
|
32
37
|
get: (id: string) => objs.get(id),
|
|
33
38
|
[Symbol.iterator]: () => objs[Symbol.iterator](),
|
|
34
39
|
},
|
|
40
|
+
getAnnotations: () => Promise.resolve(said.annotations ?? []),
|
|
35
41
|
getViewport: () => ({
|
|
36
42
|
width: 612,
|
|
37
43
|
height: H,
|
|
@@ -40,8 +46,23 @@ function page(items: FakeItem[], fonts: Record<string, unknown>) {
|
|
|
40
46
|
}),
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
|
-
const doc = (
|
|
49
|
+
const doc = (
|
|
50
|
+
pages: ReturnType<typeof page>[],
|
|
51
|
+
info: unknown = {},
|
|
52
|
+
destinations: unknown = {},
|
|
53
|
+
outline: unknown = null,
|
|
54
|
+
) => ({
|
|
44
55
|
numPages: pages.length,
|
|
56
|
+
getMetadata: () => Promise.resolve({ info, metadata: null }),
|
|
57
|
+
getDestinations: () => Promise.resolve(destinations),
|
|
58
|
+
getDestination: (id: string) =>
|
|
59
|
+
Promise.resolve(fieldOf(destinations, id) ?? null),
|
|
60
|
+
getOutline: () => Promise.resolve(outline),
|
|
61
|
+
// A reference is `{ page }` here; one without a page is one pdf.js cannot place.
|
|
62
|
+
getPageIndex: (ref: object) =>
|
|
63
|
+
"page" in ref && typeof ref.page === "number"
|
|
64
|
+
? Promise.resolve(ref.page - 1)
|
|
65
|
+
: Promise.reject(new Error("Invalid page reference")),
|
|
45
66
|
getPage: (i: number) =>
|
|
46
67
|
Promise.resolve(present(pages[i - 1], `page ${String(i)}`)),
|
|
47
68
|
});
|
|
@@ -152,3 +173,90 @@ test("🔴 a rejection with no value at all is a failure too, not a TypeError ou
|
|
|
152
173
|
],
|
|
153
174
|
);
|
|
154
175
|
});
|
|
176
|
+
|
|
177
|
+
test("what the pages say: text, links with their page, metadata", async () => {
|
|
178
|
+
const fonts = { f1: { loadedName: "f1", name: "Named", ascent: 0.8 } };
|
|
179
|
+
const plain = page([item("f1", [10, 0, 0, 10, 50, 700])], fonts);
|
|
180
|
+
const linked = page([item("f1", [10, 0, 0, 10, 50, 700])], fonts, {
|
|
181
|
+
annotations: [{ subtype: "Link", url: "https://github.com/adaexample" }],
|
|
182
|
+
});
|
|
183
|
+
const r = await factsOf(doc([plain, linked], { Author: "Ada Example" }), lib);
|
|
184
|
+
assert.ok(r.ok);
|
|
185
|
+
assert.deepEqual(
|
|
186
|
+
[r.facts.pageTexts, r.facts.links, r.facts.metadata],
|
|
187
|
+
[
|
|
188
|
+
["word", "word"],
|
|
189
|
+
[{ page: 2, uri: "https://github.com/adaexample" }],
|
|
190
|
+
{ Author: "Ada Example" },
|
|
191
|
+
],
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("the bibliography anchor: the lowest page of any cite.<key> destination", async () => {
|
|
196
|
+
const fonts = { f1: { loadedName: "f1", name: "Named", ascent: 0.8 } };
|
|
197
|
+
const pages = [1, 2, 3].map(() =>
|
|
198
|
+
page([item("f1", [10, 0, 0, 10, 50, 700])], fonts),
|
|
199
|
+
);
|
|
200
|
+
const facts = async (destinations: unknown) => {
|
|
201
|
+
const r = await factsOf(doc(pages, {}, destinations), lib);
|
|
202
|
+
assert.ok(r.ok);
|
|
203
|
+
return r.facts.bibAnchorPage;
|
|
204
|
+
};
|
|
205
|
+
assert.equal(
|
|
206
|
+
await facts({
|
|
207
|
+
"cite.b": [{ page: 3 }, { name: "XYZ" }],
|
|
208
|
+
"cite.a": [{ page: 2 }, { name: "XYZ" }],
|
|
209
|
+
"section.1": [{ page: 1 }, { name: "XYZ" }],
|
|
210
|
+
// A reference pdf.js cannot place is skipped, not fatal.
|
|
211
|
+
"cite.c": [{ num: 99 }, { name: "XYZ" }],
|
|
212
|
+
}),
|
|
213
|
+
2,
|
|
214
|
+
);
|
|
215
|
+
// No hyperref: no cite destinations, and the value says so.
|
|
216
|
+
assert.equal(await facts({ "section.1": [{ page: 1 }] }), null);
|
|
217
|
+
assert.equal(await facts(null), null);
|
|
218
|
+
// A destination that is not an explicit array is not an anchor.
|
|
219
|
+
assert.equal(await facts({ "cite.a": "named-elsewhere" }), null);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("the appendix anchor: an appendix.<letter> destination, or a bookmark titled «Appendix…»", async () => {
|
|
223
|
+
const fonts = { f1: { loadedName: "f1", name: "Named", ascent: 0.8 } };
|
|
224
|
+
const pages = [1, 2, 3, 4].map(() =>
|
|
225
|
+
page([item("f1", [10, 0, 0, 10, 50, 700])], fonts),
|
|
226
|
+
);
|
|
227
|
+
const facts = async (destinations: unknown, outline: unknown) => {
|
|
228
|
+
const r = await factsOf(doc(pages, {}, destinations, outline), lib);
|
|
229
|
+
assert.ok(r.ok);
|
|
230
|
+
return r.facts.appendixAnchorPage;
|
|
231
|
+
};
|
|
232
|
+
const XYZ = { name: "XYZ" };
|
|
233
|
+
// The standard classes: hyperref names the appendix's sections `appendix.<letter>`.
|
|
234
|
+
assert.equal(
|
|
235
|
+
await facts(
|
|
236
|
+
{ "appendix.B": [{ page: 4 }, XYZ], "appendix.A": [{ page: 3 }, XYZ] },
|
|
237
|
+
null,
|
|
238
|
+
),
|
|
239
|
+
3,
|
|
240
|
+
);
|
|
241
|
+
// IEEEtran's \appendices: the sections are `section*.<n>`; the bookmark's title says appendix,
|
|
242
|
+
// its destination is a name to look up, or an explicit one.
|
|
243
|
+
const named = {
|
|
244
|
+
"section*.5": [{ page: 2 }, XYZ],
|
|
245
|
+
"section*.4": [{ page: 1 }, XYZ],
|
|
246
|
+
};
|
|
247
|
+
assert.equal(
|
|
248
|
+
await facts(named, [
|
|
249
|
+
{ title: "Conclusion", dest: "section*.4", items: [] },
|
|
250
|
+
{ title: "Appendix A: Details", dest: "section*.5", items: [] },
|
|
251
|
+
{ title: "APPENDIX", dest: [{ page: 4 }, XYZ], items: [] },
|
|
252
|
+
]),
|
|
253
|
+
2,
|
|
254
|
+
);
|
|
255
|
+
// No appendix anywhere, a bookmark whose name resolves to nothing, and no outline at all.
|
|
256
|
+
assert.equal(
|
|
257
|
+
await facts(named, [{ title: "Conclusion", dest: "section*.4" }]),
|
|
258
|
+
null,
|
|
259
|
+
);
|
|
260
|
+
assert.equal(await facts({}, [{ title: "Appendices", dest: "gone" }]), null);
|
|
261
|
+
assert.equal(await facts({}, { not: "a list" }), null);
|
|
262
|
+
});
|