paperlint 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +16 -23
- package/CLAUDE.md +55 -11
- package/CONTRIBUTING.md +30 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +23 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +110 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +12 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +42 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +33 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +25 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +58 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +97 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +95 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +119 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +7 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +53 -1
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +35 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/tex-paper.d.ts +19 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +105 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +88 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +268 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/missing-input.md +58 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +75 -0
- package/docs/rules/tex/template.md +74 -0
- package/docs/rules/tex/venue-leftover.md +73 -0
- package/docs/rules.md +73 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +156 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +14 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +4 -4
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +155 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +535 -0
- package/src/adapters/latex/index.ts +65 -0
- package/src/adapters/latex/nodes.ts +150 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +137 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +53 -8
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +143 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +42 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +524 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +741 -0
- package/src/tex-venue-rules.ts +461 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
package/src/cli.ts
CHANGED
|
@@ -32,10 +32,8 @@ import { join, dirname, resolve, relative, basename, sep } from "node:path";
|
|
|
32
32
|
import markdown from "@eslint/markdown";
|
|
33
33
|
import { z } from "zod";
|
|
34
34
|
// Types come from consumer.d.mts beside it.
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
packageVenuesDir,
|
|
38
|
-
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
35
|
+
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
36
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
39
37
|
export { isMain };
|
|
40
38
|
import type { Args, PaperlintConfig, ConfigRead } from "./types.ts";
|
|
41
39
|
import {
|
|
@@ -48,8 +46,13 @@ import { prepareEngine } from "./build-engine.ts";
|
|
|
48
46
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
49
47
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
50
48
|
import { curlDownload } from "./adapters/curl/index.ts";
|
|
49
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
51
50
|
import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
|
|
52
51
|
import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
|
|
52
|
+
import { TEX_VENUE_RULE_LEVELS, texVenueRules } from "./tex-venue-rules.ts";
|
|
53
|
+
import { claimProvenanceRule } from "./claim-provenance.ts";
|
|
54
|
+
import { registerRule } from "./register.ts";
|
|
55
|
+
import { missingInputRule } from "./tex-paper.ts";
|
|
53
56
|
import {
|
|
54
57
|
paperRules,
|
|
55
58
|
stringFields,
|
|
@@ -72,6 +75,7 @@ import {
|
|
|
72
75
|
shippedPresets,
|
|
73
76
|
SHIPPED_PREFIX,
|
|
74
77
|
type PaperPreset,
|
|
78
|
+
type PresetTemplate,
|
|
75
79
|
} from "./presets.ts";
|
|
76
80
|
import type { ToolInstaller } from "./ports/tool-installer.ts";
|
|
77
81
|
import {
|
|
@@ -132,7 +136,7 @@ import { messageOf } from "./domain/text.ts";
|
|
|
132
136
|
import { isRecord } from "./domain/record.ts";
|
|
133
137
|
|
|
134
138
|
/** The shipped venue presets, read from the package's venues directory — the one list. */
|
|
135
|
-
const SHIPPED_VENUES = (): string[] => shippedPresets(
|
|
139
|
+
const SHIPPED_VENUES = (): string[] => shippedPresets(presetsDir());
|
|
136
140
|
|
|
137
141
|
const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
138
142
|
|
|
@@ -257,7 +261,7 @@ export function buildConfig(
|
|
|
257
261
|
pdf: {
|
|
258
262
|
rules: {
|
|
259
263
|
...pdfRules.rules,
|
|
260
|
-
...venueRules({ files: nodeFiles, venuesDir:
|
|
264
|
+
...venueRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
261
265
|
},
|
|
262
266
|
},
|
|
263
267
|
},
|
|
@@ -302,7 +306,29 @@ export function buildConfig(
|
|
|
302
306
|
cfg.push({
|
|
303
307
|
files: ["**/paper.tex"],
|
|
304
308
|
plugins: {
|
|
305
|
-
tex: {
|
|
309
|
+
tex: {
|
|
310
|
+
languages: { latex: texLanguage },
|
|
311
|
+
rules: {
|
|
312
|
+
...texBuild,
|
|
313
|
+
...texVenueRules({
|
|
314
|
+
files: nodeFiles,
|
|
315
|
+
venuesDir: presetsDir(),
|
|
316
|
+
latex: latexReader,
|
|
317
|
+
}),
|
|
318
|
+
"claim-provenance": claimProvenanceRule({
|
|
319
|
+
files: nodeFiles,
|
|
320
|
+
latex: latexReader,
|
|
321
|
+
}),
|
|
322
|
+
"missing-input": missingInputRule({
|
|
323
|
+
files: nodeFiles,
|
|
324
|
+
latex: latexReader,
|
|
325
|
+
}),
|
|
326
|
+
register: registerRule({
|
|
327
|
+
files: nodeFiles,
|
|
328
|
+
latex: latexReader,
|
|
329
|
+
}),
|
|
330
|
+
},
|
|
331
|
+
},
|
|
306
332
|
paper: { rules: texPaperRules },
|
|
307
333
|
bib: bibReachable,
|
|
308
334
|
},
|
|
@@ -313,9 +339,15 @@ export function buildConfig(
|
|
|
313
339
|
"paper/figure-ref-style": "warn",
|
|
314
340
|
"bib/reachable-entry": "warn",
|
|
315
341
|
"tex/future-promise": "warn",
|
|
342
|
+
// A number with no owner: a prose rule, on beside the others above.
|
|
343
|
+
"tex/claim-provenance": "warn",
|
|
344
|
+
"tex/missing-input": "warn",
|
|
345
|
+
// Sentence-initial conjunctions over the whole body: register, not vocabulary.
|
|
346
|
+
"tex/register": "warn",
|
|
316
347
|
"tex/acm-frontmatter-override": "error",
|
|
317
348
|
// Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
|
|
318
349
|
...VENUE_RULE_LEVELS,
|
|
350
|
+
...TEX_VENUE_RULE_LEVELS,
|
|
319
351
|
},
|
|
320
352
|
});
|
|
321
353
|
// 🔴 ONLY THE FILES THESE BLOCKS CLAIM ARE LINTED (src/paper-files.ts). Without this block ESLint's
|
|
@@ -1037,12 +1069,25 @@ export async function chooseVenue(
|
|
|
1037
1069
|
value: {
|
|
1038
1070
|
extends: spec.value,
|
|
1039
1071
|
kind: kind !== null && kinds.includes(kind) ? kind : null,
|
|
1072
|
+
setClass: classSetter(r.value.template),
|
|
1040
1073
|
label,
|
|
1041
1074
|
kinds,
|
|
1042
1075
|
},
|
|
1043
1076
|
};
|
|
1044
1077
|
}
|
|
1045
1078
|
|
|
1079
|
+
/**
|
|
1080
|
+
* What `paperlint new` does to the template's paper.tex for a venue: set it in the class the
|
|
1081
|
+
* preset's template names. Null when the preset names no template, or one the reader cannot read
|
|
1082
|
+
* (then `tex/template` says so on the new paper).
|
|
1083
|
+
*/
|
|
1084
|
+
function classSetter(
|
|
1085
|
+
t: PresetTemplate | null,
|
|
1086
|
+
): ((paperTex: string) => string) | null {
|
|
1087
|
+
const cls = t === null ? null : latexReader.template(t.text);
|
|
1088
|
+
return cls === null ? null : (src) => latexReader.withDocumentClass(src, cls);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1046
1091
|
const bad = (error: string): { ok: false; error: string } => ({
|
|
1047
1092
|
ok: false,
|
|
1048
1093
|
error,
|
|
@@ -1217,7 +1262,7 @@ async function runBuild(
|
|
|
1217
1262
|
}
|
|
1218
1263
|
|
|
1219
1264
|
/** The presets' deps, wired to the disk and the package's own venues directory. */
|
|
1220
|
-
const PRESET_DEPS = { files: nodeFiles, venuesDir:
|
|
1265
|
+
const PRESET_DEPS = { files: nodeFiles, venuesDir: presetsDir() };
|
|
1221
1266
|
|
|
1222
1267
|
/**
|
|
1223
1268
|
* What one paper needs from TeX Live: the base set plus its preset chain's `tex`. A paper whose
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paper as one text: includes spliced where they stand, every character mapped back to its
|
|
3
|
+
* file, a missing file reported, a subfile's body only, and a file that includes itself entered once.
|
|
4
|
+
* The reader is a stand-in that finds `\input{…}` by position, so these tests are about assembling,
|
|
5
|
+
* not about parsing LaTeX (the LaTeX adapter's own tests cover `includes`).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
assemblePaper,
|
|
10
|
+
originOf,
|
|
11
|
+
type AssembleDeps,
|
|
12
|
+
type Include,
|
|
13
|
+
} from "./paper-source.ts";
|
|
14
|
+
|
|
15
|
+
/** `\input{x}` / `\include{x}` / `\subfile{x}` in `src`, by a scan good enough for these inputs. */
|
|
16
|
+
const includes = (src: string): readonly Include[] =>
|
|
17
|
+
[...src.matchAll(/\\(input|include|subfile)\{([^}]*)\}/gu)].map((m) => ({
|
|
18
|
+
macro:
|
|
19
|
+
m[1] === "subfile" ? "subfile" : m[1] === "include" ? "include" : "input",
|
|
20
|
+
target: m[2] ?? "",
|
|
21
|
+
span: { start: m.index, end: m.index + m[0].length },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const deps = (files: Record<string, string>): AssembleDeps => ({
|
|
25
|
+
includes,
|
|
26
|
+
documentBody: (src) => {
|
|
27
|
+
const a = src.indexOf("\\begin{document}");
|
|
28
|
+
const b = src.indexOf("\\end{document}");
|
|
29
|
+
return a < 0 || b < 0
|
|
30
|
+
? null
|
|
31
|
+
: { start: a + "\\begin{document}".length, end: b };
|
|
32
|
+
},
|
|
33
|
+
read: (p) => files[p] ?? null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("assemblePaper — the text TeX reads", () => {
|
|
37
|
+
it("splices each file where its include stands; `.tex` is tried first", () => {
|
|
38
|
+
const main = "A\\input{s/one}B\\include{two.tex}C";
|
|
39
|
+
const p = assemblePaper(
|
|
40
|
+
"paper.tex",
|
|
41
|
+
main,
|
|
42
|
+
deps({ "s/one.tex": "one", "two.tex": "two" }),
|
|
43
|
+
);
|
|
44
|
+
expect(p.text).toBe("AoneBtwoC");
|
|
45
|
+
expect(p.missing).toEqual([]);
|
|
46
|
+
expect([...new Set(p.segments.map((g) => g.file))]).toEqual([
|
|
47
|
+
"paper.tex",
|
|
48
|
+
"s/one.tex",
|
|
49
|
+
"two.tex",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("nested includes resolve from the paper directory, and map back through the outermost include", () => {
|
|
54
|
+
const main = "x\\input{a}y";
|
|
55
|
+
const p = assemblePaper(
|
|
56
|
+
"paper.tex",
|
|
57
|
+
main,
|
|
58
|
+
deps({ "a.tex": "[\\input{s/b}]", "s/b.tex": "BB" }),
|
|
59
|
+
);
|
|
60
|
+
expect(p.text).toBe("x[BB]y");
|
|
61
|
+
expect(originOf(p, { start: 2, end: 4 })).toEqual({
|
|
62
|
+
file: "s/b.tex",
|
|
63
|
+
source: "BB",
|
|
64
|
+
span: { start: 0, end: 2 },
|
|
65
|
+
via: { start: 1, end: 10 },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("🔴 a missing file is reported where it stands, and left out", () => {
|
|
70
|
+
const main = "a\\input{gone}b";
|
|
71
|
+
const p = assemblePaper("paper.tex", main, deps({}));
|
|
72
|
+
expect(p.text).toBe("ab");
|
|
73
|
+
expect(p.missing).toEqual([
|
|
74
|
+
{
|
|
75
|
+
file: "paper.tex",
|
|
76
|
+
target: "gone",
|
|
77
|
+
span: { start: 1, end: 13 },
|
|
78
|
+
via: { start: 1, end: 13 },
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("assemblePaper — subfiles and a file that includes itself", () => {
|
|
85
|
+
it("a subfile contributes its document body, not its preamble", () => {
|
|
86
|
+
const sub =
|
|
87
|
+
"\\documentclass[paper]{subfiles}\\begin{document}BODY\\end{document}";
|
|
88
|
+
const p = assemblePaper(
|
|
89
|
+
"paper.tex",
|
|
90
|
+
"<\\subfile{s}>",
|
|
91
|
+
deps({ "s.tex": sub }),
|
|
92
|
+
);
|
|
93
|
+
expect(p.text).toBe("<BODY>");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("a subfile with no document environment is read whole", () => {
|
|
97
|
+
const p = assemblePaper(
|
|
98
|
+
"paper.tex",
|
|
99
|
+
"<\\subfile{s}>",
|
|
100
|
+
deps({ "s.tex": "ALL" }),
|
|
101
|
+
);
|
|
102
|
+
expect(p.text).toBe("<ALL>");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("a file that includes itself is entered once", () => {
|
|
106
|
+
const p = assemblePaper(
|
|
107
|
+
"paper.tex",
|
|
108
|
+
"m\\input{a}",
|
|
109
|
+
deps({ "a.tex": "a\\input{a}\\input{paper.tex}" }),
|
|
110
|
+
);
|
|
111
|
+
expect(p.text).toBe("ma");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("originOf — where a span of the assembled text comes from", () => {
|
|
116
|
+
const p = assemblePaper(
|
|
117
|
+
"paper.tex",
|
|
118
|
+
"ab\\input{x}cd",
|
|
119
|
+
deps({ "x.tex": "XYZ" }),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
it("in the main file: its own offsets, via nothing", () => {
|
|
123
|
+
expect(originOf(p, { start: 0, end: 2 })).toEqual({
|
|
124
|
+
file: "paper.tex",
|
|
125
|
+
source: "ab\\input{x}cd",
|
|
126
|
+
span: { start: 0, end: 2 },
|
|
127
|
+
via: null,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("after the include: offsets in the main file past the macro", () => {
|
|
132
|
+
expect(originOf(p, { start: 5, end: 7 })).toEqual({
|
|
133
|
+
file: "paper.tex",
|
|
134
|
+
source: "ab\\input{x}cd",
|
|
135
|
+
span: { start: 11, end: 13 },
|
|
136
|
+
via: null,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("a span running out of an included file is cut at its end", () => {
|
|
141
|
+
expect(originOf(p, { start: 3, end: 7 })).toEqual({
|
|
142
|
+
file: "x.tex",
|
|
143
|
+
source: "XYZ",
|
|
144
|
+
span: { start: 1, end: 3 },
|
|
145
|
+
via: { start: 2, end: 11 },
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("an empty span at the very end belongs to the last stretch; outside the text there is none", () => {
|
|
150
|
+
expect(originOf(p, { start: 7, end: 7 })?.span).toEqual({
|
|
151
|
+
start: 13,
|
|
152
|
+
end: 13,
|
|
153
|
+
});
|
|
154
|
+
expect(originOf(p, { start: 9, end: 9 })).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A paper as one text: `paper.tex` with every `\input`, `\include` and `\subfile` replaced by the
|
|
3
|
+
* file it names — what TeX itself does — and a map from every character of that text back to the
|
|
4
|
+
* file and offset it came from. The rules read the whole paper; a finding still points at its file.
|
|
5
|
+
*
|
|
6
|
+
* ── WHY SPLICE THE TEXT ──────────────────────────────────────────────────────────
|
|
7
|
+
* `\input` is textual inclusion: TeX reads the named file where the macro stands. Replacing the
|
|
8
|
+
* macro by the file's text gives the parser exactly what TeX reads, so every question the reader
|
|
9
|
+
* answers about one file (the class, the outline, the rendered text, the body's passages) is
|
|
10
|
+
* answered about the paper, unchanged. A subfile is a document of its own; only its body is spliced.
|
|
11
|
+
*
|
|
12
|
+
* ── WHAT IS NOT DONE ─────────────────────────────────────────────────────────────
|
|
13
|
+
* No macro is expanded, so a path built from a macro (`\input{\dir/intro}`) is read as written. A
|
|
14
|
+
* file that cannot be found is reported and left out; a file that includes itself, directly or
|
|
15
|
+
* through others, is spliced once.
|
|
16
|
+
*/
|
|
17
|
+
import { extname, normalize } from "node:path";
|
|
18
|
+
import type { Span } from "./tex-document.ts";
|
|
19
|
+
|
|
20
|
+
/** One `\input`, `\include` or `\subfile` in a source: the path as written and where the macro stands. */
|
|
21
|
+
export interface Include {
|
|
22
|
+
readonly macro: "input" | "include" | "subfile";
|
|
23
|
+
readonly target: string;
|
|
24
|
+
readonly span: Span;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What assembling needs from the LaTeX reader and from the disk, relative to the paper directory. */
|
|
28
|
+
export interface AssembleDeps {
|
|
29
|
+
/** The includes of a source, in source order. */
|
|
30
|
+
readonly includes: (src: string) => readonly Include[];
|
|
31
|
+
/** The span of a source's `document` environment body, or null when it has none. */
|
|
32
|
+
readonly documentBody: (src: string) => Span | null;
|
|
33
|
+
/** A file's text by its path relative to the paper directory, or null when there is none. */
|
|
34
|
+
readonly read: (path: string) => string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A stretch of the assembled text copied from one file: `[start, end)` here is `from…` there. */
|
|
38
|
+
export interface Segment {
|
|
39
|
+
readonly start: number;
|
|
40
|
+
readonly end: number;
|
|
41
|
+
readonly file: string;
|
|
42
|
+
/** The whole text of `file`, so a place in it can be told as a line and a column. */
|
|
43
|
+
readonly source: string;
|
|
44
|
+
readonly from: number;
|
|
45
|
+
/** Where, in the main file, the outermost include that brought this text in stands. */
|
|
46
|
+
readonly via: Span | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An include whose file was not found, where it stands. */
|
|
50
|
+
export interface MissingInclude {
|
|
51
|
+
readonly file: string;
|
|
52
|
+
readonly target: string;
|
|
53
|
+
readonly span: Span;
|
|
54
|
+
/** Where it stands in the main file (the include itself, or the one that brought its file in). */
|
|
55
|
+
readonly via: Span;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The paper as one text, the map back to its files, and the includes that were not found. */
|
|
59
|
+
export interface PaperSource {
|
|
60
|
+
readonly main: string;
|
|
61
|
+
readonly text: string;
|
|
62
|
+
readonly segments: readonly Segment[];
|
|
63
|
+
readonly missing: readonly MissingInclude[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The paths TeX tries for a target: `name.tex`, then `name` — or the name alone if it has one. */
|
|
67
|
+
const candidates = (target: string): readonly string[] => {
|
|
68
|
+
const t = normalize(target.trim());
|
|
69
|
+
return extname(t) === "" ? [`${t}.tex`, t] : [t];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The first candidate path that exists, with its text. */
|
|
73
|
+
function found(
|
|
74
|
+
target: string,
|
|
75
|
+
read: AssembleDeps["read"],
|
|
76
|
+
): { readonly path: string; readonly text: string } | null {
|
|
77
|
+
const hits = candidates(target).flatMap((path) => {
|
|
78
|
+
const text = read(path);
|
|
79
|
+
return text === null ? [] : [{ path, text }];
|
|
80
|
+
});
|
|
81
|
+
return hits[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An assembled piece of text: the text, its segments from 0, and the includes not found. */
|
|
85
|
+
interface Piece {
|
|
86
|
+
readonly text: string;
|
|
87
|
+
readonly segments: readonly Segment[];
|
|
88
|
+
readonly missing: readonly MissingInclude[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `segments` moved by `by` characters. */
|
|
92
|
+
const shifted = (
|
|
93
|
+
segments: readonly Segment[],
|
|
94
|
+
by: number,
|
|
95
|
+
): readonly Segment[] =>
|
|
96
|
+
segments.map((s) => ({ ...s, start: s.start + by, end: s.end + by }));
|
|
97
|
+
|
|
98
|
+
/** `[from, to)` of `file`'s text, copied as it is, brought in via `via`. */
|
|
99
|
+
interface Stretch {
|
|
100
|
+
readonly file: string;
|
|
101
|
+
readonly text: string;
|
|
102
|
+
readonly from: number;
|
|
103
|
+
readonly to: number;
|
|
104
|
+
readonly via: Span | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A stretch as a piece: its text, and one segment mapping it back. */
|
|
108
|
+
const copied = ({ file, text, from, to, via }: Stretch): Piece => ({
|
|
109
|
+
text: text.slice(from, to),
|
|
110
|
+
segments:
|
|
111
|
+
to > from
|
|
112
|
+
? [{ start: 0, end: to - from, file, source: text, from, via }]
|
|
113
|
+
: [],
|
|
114
|
+
missing: [],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** Nothing: an include left out. */
|
|
118
|
+
const NOTHING: Piece = { text: "", segments: [], missing: [] };
|
|
119
|
+
|
|
120
|
+
/** Pieces joined in order. */
|
|
121
|
+
const joined = (pieces: readonly Piece[]): Piece =>
|
|
122
|
+
pieces.reduce<Piece>(
|
|
123
|
+
(acc, p) => ({
|
|
124
|
+
text: acc.text + p.text,
|
|
125
|
+
segments: [...acc.segments, ...shifted(p.segments, acc.text.length)],
|
|
126
|
+
missing: [...acc.missing, ...p.missing],
|
|
127
|
+
}),
|
|
128
|
+
NOTHING,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `[from, to)` of `file`, its includes spliced. `stack` is the chain of files being spliced, so a
|
|
133
|
+
* file that includes itself is not entered again; `via` is where the chain began in the main file.
|
|
134
|
+
*/
|
|
135
|
+
/** What splicing carries down the chain of files. */
|
|
136
|
+
interface Context {
|
|
137
|
+
readonly deps: AssembleDeps;
|
|
138
|
+
readonly stack: readonly string[];
|
|
139
|
+
readonly via: Span | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
|
|
143
|
+
const incs = ctx.deps
|
|
144
|
+
.includes(text)
|
|
145
|
+
.filter((i) => i.span.start >= range.start && i.span.end <= range.end);
|
|
146
|
+
const at = (from: number, to: number): Piece =>
|
|
147
|
+
copied({ file, text, from, to, via: ctx.via });
|
|
148
|
+
const { pieces, cursor } = incs.reduce<{
|
|
149
|
+
readonly pieces: readonly Piece[];
|
|
150
|
+
readonly cursor: number;
|
|
151
|
+
}>(
|
|
152
|
+
(acc, inc) => ({
|
|
153
|
+
pieces: [
|
|
154
|
+
...acc.pieces,
|
|
155
|
+
at(acc.cursor, inc.span.start),
|
|
156
|
+
included(file, inc, ctx),
|
|
157
|
+
],
|
|
158
|
+
cursor: inc.span.end,
|
|
159
|
+
}),
|
|
160
|
+
{ pieces: [], cursor: range.start },
|
|
161
|
+
);
|
|
162
|
+
return joined([...pieces, at(cursor, range.end)]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One include: the file it names, spliced, or — when not found — nothing, and a report. */
|
|
166
|
+
function included(file: string, inc: Include, ctx: Context): Piece {
|
|
167
|
+
const via = ctx.via ?? inc.span;
|
|
168
|
+
const hit = found(inc.target, ctx.deps.read);
|
|
169
|
+
if (hit === null)
|
|
170
|
+
return {
|
|
171
|
+
...NOTHING,
|
|
172
|
+
missing: [{ file, target: inc.target, span: inc.span, via }],
|
|
173
|
+
};
|
|
174
|
+
if (ctx.stack.includes(hit.path)) return NOTHING;
|
|
175
|
+
const body = inc.macro === "subfile" ? ctx.deps.documentBody(hit.text) : null;
|
|
176
|
+
return spliced(
|
|
177
|
+
hit.path,
|
|
178
|
+
hit.text,
|
|
179
|
+
body ?? { start: 0, end: hit.text.length },
|
|
180
|
+
{ deps: ctx.deps, stack: [...ctx.stack, hit.path], via },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The paper whose main file is `main` (a path relative to the paper directory) with `text`. */
|
|
185
|
+
export function assemblePaper(
|
|
186
|
+
main: string,
|
|
187
|
+
text: string,
|
|
188
|
+
deps: AssembleDeps,
|
|
189
|
+
): PaperSource {
|
|
190
|
+
const p = spliced(
|
|
191
|
+
main,
|
|
192
|
+
text,
|
|
193
|
+
{ start: 0, end: text.length },
|
|
194
|
+
{ deps, stack: [main], via: null },
|
|
195
|
+
);
|
|
196
|
+
return {
|
|
197
|
+
main,
|
|
198
|
+
text: p.text,
|
|
199
|
+
segments: p.segments,
|
|
200
|
+
missing: p.missing,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Where a span of the assembled text stands: in which file, at which offsets, and via which include. */
|
|
205
|
+
export interface Origin {
|
|
206
|
+
readonly file: string;
|
|
207
|
+
/** The whole text of `file`. */
|
|
208
|
+
readonly source: string;
|
|
209
|
+
readonly span: Span;
|
|
210
|
+
readonly via: Span | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The origin of `span` of the assembled text: the file of its first character. A span that runs
|
|
215
|
+
* past that file's stretch is cut at its end — a sentence cannot point into two files.
|
|
216
|
+
*/
|
|
217
|
+
export function originOf(p: PaperSource, span: Span): Origin | null {
|
|
218
|
+
// The character at `start`; an empty span at the very end belongs to the last stretch.
|
|
219
|
+
const seg =
|
|
220
|
+
p.segments.find((s) => span.start >= s.start && span.start < s.end) ??
|
|
221
|
+
p.segments.find((s) => span.start === s.end);
|
|
222
|
+
if (seg === undefined) return null;
|
|
223
|
+
const end = Math.min(span.end, seg.end);
|
|
224
|
+
return {
|
|
225
|
+
file: seg.file,
|
|
226
|
+
source: seg.source,
|
|
227
|
+
span: {
|
|
228
|
+
start: seg.from + span.start - seg.start,
|
|
229
|
+
end: seg.from + end - seg.start,
|
|
230
|
+
},
|
|
231
|
+
via: seg.via,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentences of body prose — one splitter, so every rule that counts or judges sentences cuts
|
|
3
|
+
* them the same way (`tex/claim-provenance` judges each; `tex/register` counts them).
|
|
4
|
+
*
|
|
5
|
+
* A passage becomes one text: its prose as itself, and each mark (a citation, a cross-reference) or
|
|
6
|
+
* math as one character no prose contains, where it stands. `Intl.Segmenter` cuts that text; a break
|
|
7
|
+
* after an abbreviation is undone, and marks set right after a full stop belong to the sentence the
|
|
8
|
+
* full stop ends.
|
|
9
|
+
*/
|
|
10
|
+
import type { Passage, ProsePiece, Segment, TextRun } from "./tex-document.ts";
|
|
11
|
+
|
|
12
|
+
/** What stands in a sentence's text for a mark: characters no prose contains (private use). */
|
|
13
|
+
export const CITATION = "";
|
|
14
|
+
export const REFERENCE = "";
|
|
15
|
+
export const MATH = "";
|
|
16
|
+
|
|
17
|
+
/** A piece as the text of its sentence sees it: text as itself, a mark or math as one character. */
|
|
18
|
+
export function segmentOf(p: ProsePiece): Segment {
|
|
19
|
+
switch (p.kind) {
|
|
20
|
+
case "text":
|
|
21
|
+
return p.segment;
|
|
22
|
+
case "owner":
|
|
23
|
+
return {
|
|
24
|
+
text: p.owner === "citation" ? CITATION : REFERENCE,
|
|
25
|
+
at: p.span.start,
|
|
26
|
+
};
|
|
27
|
+
case "math":
|
|
28
|
+
return { text: MATH, at: p.span.start };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A passage as one run of text, each mark a character where it stands. */
|
|
33
|
+
export const runOf = (p: Passage): TextRun => {
|
|
34
|
+
const [first, ...rest] = p.pieces;
|
|
35
|
+
return { segments: [segmentOf(first), ...rest.map(segmentOf)] };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** An abbreviation whose full stop does not end a sentence, at the end of a segment. */
|
|
39
|
+
const ABBREVIATION =
|
|
40
|
+
/(?:^|[^\p{L}])(?:e\.g|i\.e|cf|vs|et al|Figs?|Secs?|Tab|Eqs?|No|App|approx|resp)\.\s*$/u;
|
|
41
|
+
|
|
42
|
+
const SEGMENTER = new Intl.Segmenter("en", { granularity: "sentence" });
|
|
43
|
+
|
|
44
|
+
/** Marks, and the spaces among them, that open a segment: a `\cite` or a footnote set after the full stop. */
|
|
45
|
+
const LEADING_MARKS = /^[\s]*[]/u;
|
|
46
|
+
|
|
47
|
+
/** `[from, to)` of a text. */
|
|
48
|
+
export type Range = readonly [number, number];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sentences of `text`, as `[from, to)` ranges. A break after an abbreviation is undone, and
|
|
52
|
+
* marks standing right after a full stop belong to the sentence the full stop ends.
|
|
53
|
+
*/
|
|
54
|
+
export function sentences(text: string): readonly Range[] {
|
|
55
|
+
return [...SEGMENTER.segment(text)].reduce<readonly Range[]>((acc, s) => {
|
|
56
|
+
const prev = acc.at(-1);
|
|
57
|
+
const end = s.index + s.segment.length;
|
|
58
|
+
if (prev === undefined) return [[s.index, end]];
|
|
59
|
+
const before = acc.slice(0, -1);
|
|
60
|
+
if (ABBREVIATION.test(text.slice(...prev)))
|
|
61
|
+
return [...before, [prev[0], end]];
|
|
62
|
+
const split = s.index + (LEADING_MARKS.exec(s.segment)?.[0].length ?? 0);
|
|
63
|
+
return split < end
|
|
64
|
+
? [...before, [prev[0], split], [split, end]]
|
|
65
|
+
: [...before, [prev[0], end]];
|
|
66
|
+
}, []);
|
|
67
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** The values a LaTeX source is read into: a run's text and the span of a range inside it. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
collapse,
|
|
5
|
+
documentClassLine,
|
|
6
|
+
missingOptions,
|
|
7
|
+
runText,
|
|
8
|
+
spanIn,
|
|
9
|
+
venueClass,
|
|
10
|
+
type TextRun,
|
|
11
|
+
} from "./tex-document.ts";
|
|
12
|
+
|
|
13
|
+
// "ab" at 10, then " " at 20 (one character standing for a longer whitespace), then "cd" at 30.
|
|
14
|
+
const RUN: TextRun = {
|
|
15
|
+
segments: [
|
|
16
|
+
{ text: "ab", at: 10 },
|
|
17
|
+
{ text: " ", at: 20 },
|
|
18
|
+
{ text: "cd", at: 30 },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("a text run", () => {
|
|
23
|
+
it("its text is its segments' text, in order", () => {
|
|
24
|
+
expect(runText(RUN)).toBe("ab cd");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("spanIn: from the first character's offset to past the last's, across segments", () => {
|
|
28
|
+
expect(spanIn(RUN, 0, 2)).toEqual({ start: 10, end: 12 });
|
|
29
|
+
expect(spanIn(RUN, 1, 4)).toEqual({ start: 11, end: 31 });
|
|
30
|
+
expect(spanIn(RUN, 3, 5)).toEqual({ start: 30, end: 32 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("🔴 spanIn: an empty range, or one reaching outside the run, has no span — never {0, 0}", () => {
|
|
34
|
+
expect(spanIn(RUN, 2, 2)).toBe(null);
|
|
35
|
+
expect(spanIn(RUN, -1, 2)).toBe(null);
|
|
36
|
+
expect(spanIn(RUN, 3, 9)).toBe(null);
|
|
37
|
+
expect(spanIn(RUN, 9, 10)).toBe(null);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("a document class", () => {
|
|
42
|
+
it("documentClassLine writes it back; with no options, no brackets", () => {
|
|
43
|
+
expect(
|
|
44
|
+
documentClassLine({
|
|
45
|
+
cls: "IEEEtran",
|
|
46
|
+
options: ["conference", "compsoc"],
|
|
47
|
+
}),
|
|
48
|
+
).toBe("\\documentclass[conference,compsoc]{IEEEtran}");
|
|
49
|
+
expect(documentClassLine({ cls: "article", options: [] })).toBe(
|
|
50
|
+
"\\documentclass{article}",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("missingOptions: every option of the wanted class the paper lacks, in the wanted order", () => {
|
|
55
|
+
expect(
|
|
56
|
+
missingOptions(
|
|
57
|
+
{ cls: "x", options: ["b"] },
|
|
58
|
+
{ cls: "x", options: ["a", "b", "c"] },
|
|
59
|
+
),
|
|
60
|
+
).toEqual(["a", "c"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("venueClass: another class is replaced; the same class gains the missing options, or is kept", () => {
|
|
64
|
+
const want = { cls: "IEEEtran", options: ["conference", "compsoc"] };
|
|
65
|
+
expect(venueClass({ cls: "article", options: ["11pt"] }, want)).toEqual({
|
|
66
|
+
kind: "replace",
|
|
67
|
+
by: want,
|
|
68
|
+
});
|
|
69
|
+
expect(
|
|
70
|
+
venueClass({ cls: "IEEEtran", options: ["review", "conference"] }, want),
|
|
71
|
+
).toEqual({ kind: "add", options: ["compsoc"] });
|
|
72
|
+
expect(
|
|
73
|
+
venueClass({ cls: "IEEEtran", options: ["compsoc", "conference"] }, want),
|
|
74
|
+
).toEqual({ kind: "keep" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("collapse: every run of whitespace, newlines included, and trims", () => {
|
|
78
|
+
expect(collapse(" LLM Usage\n Statement ")).toBe("LLM Usage Statement");
|
|
79
|
+
});
|
|
80
|
+
});
|