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.
Files changed (236) hide show
  1. package/.github/workflows/ci.yml +16 -23
  2. package/CLAUDE.md +55 -11
  3. package/CONTRIBUTING.md +30 -7
  4. package/README.md +11 -8
  5. package/dist/adapters/banal/invocation.d.ts +1 -1
  6. package/dist/adapters/latex/document-class.d.ts +23 -0
  7. package/dist/adapters/latex/document-class.d.ts.map +1 -0
  8. package/dist/adapters/latex/document-class.js +110 -0
  9. package/dist/adapters/latex/document-class.js.map +1 -0
  10. package/dist/adapters/latex/hidden.d.ts +16 -0
  11. package/dist/adapters/latex/hidden.d.ts.map +1 -0
  12. package/dist/adapters/latex/hidden.js +49 -0
  13. package/dist/adapters/latex/hidden.js.map +1 -0
  14. package/dist/adapters/latex/includes.d.ts +12 -0
  15. package/dist/adapters/latex/includes.d.ts.map +1 -0
  16. package/dist/adapters/latex/includes.js +42 -0
  17. package/dist/adapters/latex/includes.js.map +1 -0
  18. package/dist/adapters/latex/index.d.ts +33 -0
  19. package/dist/adapters/latex/index.d.ts.map +1 -0
  20. package/dist/adapters/latex/index.js +25 -0
  21. package/dist/adapters/latex/index.js.map +1 -0
  22. package/dist/adapters/latex/nodes.d.ts +58 -0
  23. package/dist/adapters/latex/nodes.d.ts.map +1 -0
  24. package/dist/adapters/latex/nodes.js +97 -0
  25. package/dist/adapters/latex/nodes.js.map +1 -0
  26. package/dist/adapters/latex/outline.d.ts +10 -0
  27. package/dist/adapters/latex/outline.d.ts.map +1 -0
  28. package/dist/adapters/latex/outline.js +39 -0
  29. package/dist/adapters/latex/outline.js.map +1 -0
  30. package/dist/adapters/latex/parse.d.ts +42 -0
  31. package/dist/adapters/latex/parse.d.ts.map +1 -0
  32. package/dist/adapters/latex/parse.js +95 -0
  33. package/dist/adapters/latex/parse.js.map +1 -0
  34. package/dist/adapters/latex/prose.d.ts +8 -0
  35. package/dist/adapters/latex/prose.d.ts.map +1 -0
  36. package/dist/adapters/latex/prose.js +353 -0
  37. package/dist/adapters/latex/prose.js.map +1 -0
  38. package/dist/adapters/latex/rendered.d.ts +10 -0
  39. package/dist/adapters/latex/rendered.d.ts.map +1 -0
  40. package/dist/adapters/latex/rendered.js +39 -0
  41. package/dist/adapters/latex/rendered.js.map +1 -0
  42. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  43. package/dist/adapters/pdfjs/fill.js +12 -12
  44. package/dist/adapters/pdfjs/fill.js.map +1 -1
  45. package/dist/build.js +4 -4
  46. package/dist/build.js.map +1 -1
  47. package/dist/claim-provenance.d.ts +34 -0
  48. package/dist/claim-provenance.d.ts.map +1 -0
  49. package/dist/claim-provenance.js +146 -0
  50. package/dist/claim-provenance.js.map +1 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +49 -5
  53. package/dist/cli.js.map +1 -1
  54. package/dist/domain/paper-source.d.ts +58 -0
  55. package/dist/domain/paper-source.d.ts.map +1 -0
  56. package/dist/domain/paper-source.js +109 -0
  57. package/dist/domain/paper-source.js.map +1 -0
  58. package/dist/domain/sentences.d.ts +26 -0
  59. package/dist/domain/sentences.d.ts.map +1 -0
  60. package/dist/domain/sentences.js +48 -0
  61. package/dist/domain/sentences.js.map +1 -0
  62. package/dist/domain/tex-document.d.ts +119 -0
  63. package/dist/domain/tex-document.d.ts.map +1 -0
  64. package/dist/domain/tex-document.js +43 -0
  65. package/dist/domain/tex-document.js.map +1 -0
  66. package/dist/eslint-rules/paper-typography.d.ts +7 -0
  67. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  68. package/dist/eslint-rules/paper-typography.js +53 -1
  69. package/dist/eslint-rules/paper-typography.js.map +1 -1
  70. package/dist/facts-file.d.ts.map +1 -1
  71. package/dist/facts-file.js +2 -2
  72. package/dist/facts-file.js.map +1 -1
  73. package/dist/new-paper.d.ts +5 -0
  74. package/dist/new-paper.d.ts.map +1 -1
  75. package/dist/new-paper.js +3 -0
  76. package/dist/new-paper.js.map +1 -1
  77. package/dist/package-dirs.d.ts +5 -0
  78. package/dist/package-dirs.d.ts.map +1 -0
  79. package/dist/package-dirs.js +14 -0
  80. package/dist/package-dirs.js.map +1 -0
  81. package/dist/ports/latex.d.ts +35 -0
  82. package/dist/ports/latex.d.ts.map +1 -0
  83. package/dist/ports/latex.js +2 -0
  84. package/dist/ports/latex.js.map +1 -0
  85. package/dist/presets.d.ts +15 -2
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -6
  88. package/dist/presets.js.map +1 -1
  89. package/dist/register.d.ts +40 -0
  90. package/dist/register.d.ts.map +1 -0
  91. package/dist/register.js +134 -0
  92. package/dist/register.js.map +1 -0
  93. package/dist/tex-paper.d.ts +19 -0
  94. package/dist/tex-paper.d.ts.map +1 -0
  95. package/dist/tex-paper.js +105 -0
  96. package/dist/tex-paper.js.map +1 -0
  97. package/dist/tex-requirements.d.ts +11 -0
  98. package/dist/tex-requirements.d.ts.map +1 -1
  99. package/dist/tex-requirements.js +14 -8
  100. package/dist/tex-requirements.js.map +1 -1
  101. package/dist/tex-venue-rules.d.ts +88 -0
  102. package/dist/tex-venue-rules.d.ts.map +1 -0
  103. package/dist/tex-venue-rules.js +268 -0
  104. package/dist/tex-venue-rules.js.map +1 -0
  105. package/dist/venue-rules.d.ts +1 -1
  106. package/dist/venue-rules.d.ts.map +1 -1
  107. package/docs/configuration.md +3 -2
  108. package/docs/e2e.md +69 -40
  109. package/docs/install.md +1 -1
  110. package/docs/prior-art/README.md +1 -1
  111. package/docs/prior-art/package-location.md +5 -5
  112. package/docs/prior-art/repro/README.md +1 -1
  113. package/docs/prior-art/test-tooling.md +21 -10
  114. package/docs/rules/tex/claim-provenance.md +83 -0
  115. package/docs/rules/tex/missing-input.md +58 -0
  116. package/docs/rules/tex/register.md +77 -0
  117. package/docs/rules/tex/required-section.md +75 -0
  118. package/docs/rules/tex/template.md +74 -0
  119. package/docs/rules/tex/venue-leftover.md +73 -0
  120. package/docs/rules.md +73 -48
  121. package/docs/testing.md +17 -4
  122. package/docs/texlive-install-decision.md +1 -1
  123. package/docs/toolchain.md +2 -2
  124. package/eslint-rules/paper-typography.test.mjs +48 -1
  125. package/eslint-rules/paper-typography.ts +72 -1
  126. package/eslint.config.mjs +156 -15
  127. package/fixtures/build-e2e/acmart/expect.json +26 -0
  128. package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
  129. package/fixtures/build-e2e/aidc/expect.json +14 -0
  130. package/fixtures/build-e2e/aidc/paper.tex +46 -0
  131. package/fixtures/build-e2e/aidc/paperlint.json +1 -0
  132. package/fixtures/build-e2e/broken/expect.json +11 -0
  133. package/fixtures/build-e2e/cite/expect.json +27 -0
  134. package/fixtures/build-e2e/empty/expect.json +12 -0
  135. package/fixtures/build-e2e/fallback/expect.json +53 -0
  136. package/fixtures/build-e2e/guards/expect.json +6 -0
  137. package/fixtures/build-e2e/no-source/expect.json +10 -0
  138. package/fixtures/build-e2e/unbalanced/expect.json +60 -0
  139. package/fixtures/pdf-facts/README.md +1 -1
  140. package/fixtures/real-markdown-paper/baseline.ts +8 -7
  141. package/fixtures/render-paper/build-clean.sh +3 -5
  142. package/package.json +14 -2
  143. package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
  144. package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
  145. package/presets/aidc.jsonc +73 -0
  146. package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
  147. package/presets/ieee-conference.jsonc +66 -0
  148. package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
  149. package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
  150. package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
  151. package/scripts/check.harness.ts +64 -10
  152. package/scripts/check.test.ts +60 -13
  153. package/scripts/check.ts +83 -30
  154. package/scripts/test.test.ts +1 -1
  155. package/scripts/test.ts +1 -1
  156. package/skills/cold-read-diff/SKILL.md +31 -10
  157. package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
  158. package/skills/draft-paper/SKILL.md +2 -2
  159. package/skills/draft-paper/SKILL.md.spec.ts +1 -1
  160. package/skills/grade-paper-writing/SKILL.md +109 -44
  161. package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
  162. package/skills/harden-paper/SKILL.md +35 -36
  163. package/skills/harden-paper/SKILL.md.spec.ts +34 -35
  164. package/skills/paper-adversarial-review/SKILL.md +2 -2
  165. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  166. package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
  167. package/skills/paper-pipeline/SKILL.md +4 -4
  168. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
  169. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
  170. package/skills/paper-pipeline/references/writing-craft.md +53 -18
  171. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
  172. package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
  173. package/skills/pc-panel-review/SKILL.md +36 -11
  174. package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
  175. package/skills/render-paper/SKILL.md +2 -2
  176. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  177. package/skills/render-paper/check-render.sh +16 -63
  178. package/skills/render-paper/render-paper.harness.mjs +17 -59
  179. package/src/adapters/banal/index.test.ts +1 -1
  180. package/src/adapters/banal/invocation.ts +1 -1
  181. package/src/adapters/banal/output.test.ts +1 -1
  182. package/src/adapters/banal/run.test.ts +1 -1
  183. package/src/adapters/banal/xml.test.ts +1 -1
  184. package/src/adapters/latex/document-class.ts +155 -0
  185. package/src/adapters/latex/hidden.ts +70 -0
  186. package/src/adapters/latex/includes.ts +66 -0
  187. package/src/adapters/latex/index.test.ts +535 -0
  188. package/src/adapters/latex/index.ts +65 -0
  189. package/src/adapters/latex/nodes.ts +150 -0
  190. package/src/adapters/latex/outline.ts +64 -0
  191. package/src/adapters/latex/parse.ts +137 -0
  192. package/src/adapters/latex/prose.ts +484 -0
  193. package/src/adapters/latex/rendered.ts +59 -0
  194. package/src/adapters/pdfjs/fill.test.ts +1 -1
  195. package/src/adapters/pdfjs/fill.ts +18 -10
  196. package/src/build.harness.mjs +5 -7
  197. package/src/build.ts +4 -4
  198. package/src/claim-provenance.test.ts +352 -0
  199. package/src/claim-provenance.ts +217 -0
  200. package/src/cli.harness.mjs +1 -1
  201. package/src/cli.test.ts +2 -8
  202. package/src/cli.ts +53 -8
  203. package/src/domain/paper-source.test.ts +156 -0
  204. package/src/domain/paper-source.ts +233 -0
  205. package/src/domain/sentences.ts +67 -0
  206. package/src/domain/tex-document.test.ts +80 -0
  207. package/src/domain/tex-document.ts +143 -0
  208. package/src/engine.harness.mjs +1 -1
  209. package/src/facts-file.test.ts +4 -7
  210. package/src/facts-file.ts +2 -2
  211. package/src/latex-loop.harness.ts +1 -1
  212. package/src/new-paper.test.ts +28 -3
  213. package/src/new-paper.ts +8 -0
  214. package/src/package-dirs.ts +17 -0
  215. package/src/paper-settings-commands.test.ts +6 -1
  216. package/src/ports/latex.ts +42 -0
  217. package/src/presets.test.ts +65 -4
  218. package/src/presets.ts +38 -15
  219. package/src/register.test.ts +165 -0
  220. package/src/register.ts +177 -0
  221. package/src/rule-cases.test.ts +524 -0
  222. package/src/rule-docs.test.ts +159 -0
  223. package/src/tex-paper.test.ts +131 -0
  224. package/src/tex-paper.ts +152 -0
  225. package/src/tex-requirements.harness.ts +13 -23
  226. package/src/tex-requirements.ts +35 -8
  227. package/src/tex-venue-rules.test.ts +741 -0
  228. package/src/tex-venue-rules.ts +461 -0
  229. package/src/toolchain.harness.mjs +1 -1
  230. package/src/venue-rules.test.ts +96 -4
  231. package/src/venue-rules.ts +1 -1
  232. package/templates/paper/paper.tex +2 -1
  233. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
  234. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
  235. /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
  236. /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
@@ -0,0 +1,155 @@
1
+ /** The paper's `\documentclass`, a preset's template, and setting a class in a source. */
2
+ import {
3
+ documentClassLine,
4
+ venueClass,
5
+ type ClassCandidate,
6
+ type ClassLine,
7
+ type DocumentClass,
8
+ } from "../../domain/tex-document.ts";
9
+ import {
10
+ collapse,
11
+ macroPlace,
12
+ mandatory,
13
+ optional,
14
+ textOf,
15
+ type Node,
16
+ type Macro,
17
+ } from "./nodes.ts";
18
+ import { parseLatex, type ParsedTex } from "./parse.ts";
19
+
20
+ const isDocumentClass = (n: Node): n is Macro =>
21
+ n.type === "macro" && n.content === "documentclass";
22
+
23
+ const isComma = (n: Node): boolean => n.type === "string" && n.content === ",";
24
+
25
+ /** Nodes split wherever `at` holds, the separators dropped: a comma inside braces is its group's. */
26
+ function splitWhere(
27
+ nodes: readonly Node[],
28
+ at: (n: Node) => boolean,
29
+ ): readonly (readonly Node[])[] {
30
+ const cuts = nodes.flatMap((n, i) => (at(n) ? [i] : []));
31
+ return [-1, ...cuts].map((from, k) =>
32
+ nodes.slice(from + 1, cuts[k] ?? nodes.length),
33
+ );
34
+ }
35
+
36
+ /** The source text a run of nodes spans; its markup-free text if unplaced. */
37
+ function sourceOf(src: string, nodes: readonly Node[]): string {
38
+ const start = nodes[0]?.position?.start.offset;
39
+ const end = nodes.at(-1)?.position?.end.offset;
40
+ return start === undefined || end === undefined
41
+ ? textOf(nodes)
42
+ : src.slice(start, end);
43
+ }
44
+
45
+ /**
46
+ * One option as TeX reads it, whitespace collapsed. A line comment is not part of it, and it joins
47
+ * its two sides with no space: `foo=bar% note⏎baz` is `foo=barbaz`.
48
+ */
49
+ const optionText = (src: string, part: readonly Node[]): string =>
50
+ collapse(
51
+ splitWhere(part, (n) => n.type === "comment")
52
+ .map((run) => sourceOf(src, run))
53
+ .join(""),
54
+ );
55
+
56
+ /** Every `\documentclass` at the top of the tree, in source order. */
57
+ const classNodes = (t: ParsedTex): readonly Macro[] =>
58
+ t.root.content.filter(isDocumentClass);
59
+
60
+ /** A `\documentclass` node as a class line: empty, or the class with its options as read. */
61
+ function classLine(src: string, node: Macro): ClassCandidate {
62
+ const place = macroPlace(node);
63
+ const cls = collapse(textOf(mandatory(node)[0]?.content));
64
+ if (cls === "") return { kind: "empty", place };
65
+ const options = splitWhere(optional(node)?.content ?? [], isComma)
66
+ .map((part) => optionText(src, part))
67
+ .filter(Boolean);
68
+ return { kind: "class", cls, options, place };
69
+ }
70
+
71
+ /**
72
+ * The `\documentclass` at the top of the tree: missing; empty (`\documentclass{}`); the class with
73
+ * each of its options as written (`foo={a,b}` stays one option, braces kept), whitespace collapsed,
74
+ * its place spanning the macro and its arguments; or, when there are several (behind a TeX switch),
75
+ * each of them as a candidate.
76
+ */
77
+ export function documentClassOf(t: ParsedTex): ClassLine {
78
+ const lines = classNodes(t).map((n) => classLine(t.src, n));
79
+ const [first, second, ...rest] = lines;
80
+ if (first === undefined) return { kind: "missing" };
81
+ return second === undefined
82
+ ? first
83
+ : { kind: "ambiguous", candidates: [first, second, ...rest] };
84
+ }
85
+
86
+ /**
87
+ * A preset's `template` → the class it names: a whole `\documentclass[…]{…}` line, or a bare class
88
+ * name (`article`), which names no option. Anything else is null.
89
+ */
90
+ export function parseTemplate(text: string): DocumentClass | null {
91
+ const t = parseLatex(text);
92
+ const [only, ...rest] = t.root.content.filter(
93
+ (n) => n.type !== "whitespace" && n.type !== "comment",
94
+ );
95
+ if (only === undefined || rest.length > 0) return null;
96
+ const line = documentClassOf(t);
97
+ if (line.kind === "class") return { cls: line.cls, options: line.options };
98
+ return only.type === "string" ? { cls: only.content, options: [] } : null;
99
+ }
100
+
101
+ /** `src` with `text` in place of `[from, to)`. */
102
+ const splice = (src: string, from: number, to: number, text: string): string =>
103
+ src.slice(0, from) + text + src.slice(to);
104
+
105
+ /**
106
+ * `options` written into the class line's own source: before the `]` of its `[…]` — right after the
107
+ * last node inside, or right after the `[` when there is none — or as a new `[…]` after the name.
108
+ * The bytes between `[` and `]` stay as the author wrote them, comments and braces included.
109
+ */
110
+ function addOptions(
111
+ src: string,
112
+ node: Macro,
113
+ line: { readonly start: number; readonly empty: boolean },
114
+ options: readonly string[],
115
+ ): string {
116
+ const list = options.join(",");
117
+ const nameEnd = line.start + 1 + node.content.length; // `\` and the macro's name
118
+ const opt = optional(node);
119
+ if (opt === undefined) return splice(src, nameEnd, nameEnd, `[${list}]`);
120
+ const close =
121
+ opt.content.at(-1)?.position?.end.offset ?? src.indexOf("[", nameEnd) + 1;
122
+ return splice(src, close, close, line.empty ? list : `,${list}`);
123
+ }
124
+
125
+ /**
126
+ * The source with its `\documentclass` line changed to satisfy `want` (see `venueClass`): another
127
+ * class is replaced by the template's line; the same class gains the options it lacks in place. A
128
+ * source whose class already satisfies `want`, that has no class line the parser could place, or
129
+ * that has several, is returned as it is.
130
+ */
131
+ export function replaceDocumentClass(
132
+ t: ParsedTex,
133
+ want: DocumentClass,
134
+ ): string {
135
+ // Several lines behind a switch: which one builds is not known, so none is rewritten.
136
+ const [node, ...others] = classNodes(t);
137
+ if (node === undefined || others.length > 0) return t.src;
138
+ const line = classLine(t.src, node);
139
+ if (line.kind !== "class" || line.place.kind === "unplaced") return t.src;
140
+ const { start, end } = line.place.span;
141
+ const change = venueClass(line, want);
142
+ switch (change.kind) {
143
+ case "keep":
144
+ return t.src;
145
+ case "replace":
146
+ return splice(t.src, start, end, documentClassLine(change.by));
147
+ case "add":
148
+ return addOptions(
149
+ t.src,
150
+ node,
151
+ { start, empty: line.options.length === 0 },
152
+ change.options,
153
+ );
154
+ }
155
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What a reader of the PDF never sees, as a walk over the tree prunes it: comments, math and code,
3
+ * the bibliography's body, and the arguments that are keys, labels, files or URLs. The rendered
4
+ * text and the outline both prune with it.
5
+ *
6
+ * A macro definition's body is NOT pruned here. It is text once the macro is used, and nothing here
7
+ * expands macros, so the rendered text reads the body where it is written — a definition naming
8
+ * another venue is a leftover whether it is used or not. The outline prunes it on top
9
+ * (`isDefinition`): a heading in a definition body is not a section where it is written.
10
+ */
11
+ import {
12
+ isArgument,
13
+ isNode,
14
+ type Argument,
15
+ type Node,
16
+ type Visited,
17
+ } from "./nodes.ts";
18
+ import { DEFINITION_MACROS, KEY_ARGUMENT, KEY_SIGNATURES } from "./parse.ts";
19
+
20
+ /** Environments whose body is not text a reader sees as prose: code, the bibliography, comments. */
21
+ const HIDDEN_ENVS: ReadonlySet<string> = new Set([
22
+ "thebibliography",
23
+ "verbatim",
24
+ "Verbatim",
25
+ "lstlisting",
26
+ "minted",
27
+ "comment",
28
+ ]);
29
+
30
+ /** Node types a reader does not see as prose: comments, code, math. */
31
+ const HIDDEN_TYPES: ReadonlySet<Node["type"]> = new Set<Node["type"]>([
32
+ "comment",
33
+ "verbatim",
34
+ "verb",
35
+ "inlinemath",
36
+ "displaymath",
37
+ "mathenv",
38
+ ]);
39
+
40
+ const KEY_MACROS: ReadonlySet<string> = new Set(Object.keys(KEY_SIGNATURES));
41
+
42
+ /** A key macro that prints none of its arguments (`\label`, `\url`, …). */
43
+ const isHiddenMacro = (name: string): boolean =>
44
+ KEY_MACROS.has(name) && !KEY_ARGUMENT.has(name);
45
+
46
+ const isHiddenNode = (n: Node): boolean =>
47
+ HIDDEN_TYPES.has(n.type) ||
48
+ (n.type === "environment" && HIDDEN_ENVS.has(n.env)) ||
49
+ (n.type === "macro" && isHiddenMacro(n.content));
50
+
51
+ /**
52
+ * The one argument of a partly printed macro (`KEY_ARGUMENT`) that is a key or a URL. What holds an
53
+ * argument is a macro or an environment; the first three tests only narrow the type.
54
+ */
55
+ const isKeyArgument = (a: Argument, parent: Visited | undefined): boolean =>
56
+ parent !== undefined &&
57
+ isNode(parent) &&
58
+ parent.type === "macro" &&
59
+ parent.args?.indexOf(a) === KEY_ARGUMENT.get(parent.content);
60
+
61
+ /** Whether the walk prunes `v` (given the node that holds it): see the module comment. */
62
+ export const isUnrendered = (
63
+ v: Visited,
64
+ parent: Visited | undefined,
65
+ ): boolean =>
66
+ isArgument(v) ? isKeyArgument(v, parent) : isNode(v) && isHiddenNode(v);
67
+
68
+ /** A macro that defines another macro or an environment (`\newcommand`, `\def`, …). */
69
+ export const isDefinition = (v: Visited): boolean =>
70
+ isNode(v) && v.type === "macro" && DEFINITION_MACROS.has(v.content);
@@ -0,0 +1,66 @@
1
+ /** The files a source pulls in, and the body of a source that is a document of its own. */
2
+ import type * as Ast from "@unified-latex/unified-latex-types";
3
+ import type { Include } from "../../domain/paper-source.ts";
4
+ import type { Span } from "../../domain/tex-document.ts";
5
+ import { isDefinition } from "./hidden.ts";
6
+ import {
7
+ inPlace,
8
+ isNode,
9
+ macroPlace,
10
+ mandatory,
11
+ placeOf,
12
+ visited,
13
+ type Macro,
14
+ type Node,
15
+ } from "./nodes.ts";
16
+ import type { ParsedTex } from "./parse.ts";
17
+
18
+ const INCLUDE_MACROS: ReadonlyMap<string, Include["macro"]> = new Map([
19
+ ["input", "input"],
20
+ ["include", "include"],
21
+ ["subfile", "subfile"],
22
+ ]);
23
+
24
+ /**
25
+ * The path an include names, as written: its braced argument, or — TeX's own form, `\input name` —
26
+ * the name up to the next space. unified-latex gives the unbraced form an argument of the first
27
+ * token only (`sections` of `sections/a`), so the source is read for it.
28
+ */
29
+ function argumentText(src: string, m: Macro): string | null {
30
+ const start = mandatory(m)[0]?.content[0]?.position?.start.offset;
31
+ if (start === undefined) return null;
32
+ const rest = src.slice(start);
33
+ const stop = rest.search(src[start - 1] === "{" ? /\}/u : /[\s{}%]/u);
34
+ return stop < 0 ? rest : rest.slice(0, stop);
35
+ }
36
+
37
+ /** One include macro as an `Include`, or nothing when it names no file or has no place. */
38
+ function includeOf(src: string, m: Macro): readonly Include[] {
39
+ const macro = INCLUDE_MACROS.get(m.content);
40
+ const target = argumentText(src, m);
41
+ if (macro === undefined || target === null || target.trim() === "") return [];
42
+ return inPlace(macroPlace(m), (span) => [{ macro, target, span }]);
43
+ }
44
+
45
+ /**
46
+ * Every `\input`, `\include` and `\subfile` of the source, in source order — not in a comment (a
47
+ * comment is a node of its own) and not in a macro definition's body, which is read where the macro
48
+ * is used, and nothing here expands macros.
49
+ */
50
+ export const includesOf = (t: ParsedTex): readonly Include[] =>
51
+ visited(t.root, isDefinition)
52
+ .filter(isNode)
53
+ .flatMap((n) => (n.type === "macro" ? includeOf(t.src, n) : []));
54
+
55
+ const isDocument = (n: Node): n is Readonly<Ast.Environment> =>
56
+ n.type === "environment" && n.env === "document";
57
+
58
+ /** The span of the `document` environment's body, or null when the source has none. */
59
+ export const documentBodyOf = (t: ParsedTex): Span | null =>
60
+ (t.root.content.find(isDocument)?.content ?? [])
61
+ .flatMap((n) => inPlace(placeOf(n), (span) => [span]))
62
+ .reduce<Span | null>(
63
+ (body, span) =>
64
+ body === null ? span : { start: body.start, end: span.end },
65
+ null,
66
+ );