paperlint 6.0.0 → 6.1.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/dist/adapters/latex/index.d.ts +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -2
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/prose.d.ts +6 -1
- package/dist/adapters/latex/prose.d.ts.map +1 -1
- package/dist/adapters/latex/prose.js +142 -31
- package/dist/adapters/latex/prose.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -18
- package/dist/cli.js.map +1 -1
- package/dist/domain/register.d.ts +96 -0
- package/dist/domain/register.d.ts.map +1 -0
- package/dist/domain/register.js +136 -0
- package/dist/domain/register.js.map +1 -0
- package/dist/domain/tex-document.d.ts +19 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +8 -3
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/ports/latex.d.ts +6 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +3 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -1
- package/dist/presets.js.map +1 -1
- package/dist/register-bands.d.ts +37 -0
- package/dist/register-bands.d.ts.map +1 -0
- package/dist/register-bands.js +196 -0
- package/dist/register-bands.js.map +1 -0
- package/dist/register.d.ts +4 -2
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +6 -45
- package/dist/register.js.map +1 -1
- package/dist/tex-requirements.d.ts +6 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +26 -6
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +1 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/rules/tex/claim-emphasis.md +71 -0
- package/docs/rules/tex/contrast-frames.md +100 -0
- package/docs/rules/tex/relation-markers.md +72 -0
- package/docs/rules.md +46 -42
- package/package.json +1 -1
- package/presets/aidc.jsonc +50 -0
- package/presets/venue-profile.schema.json +48 -1
- package/skills/grade-paper-writing/SKILL.md +59 -27
- package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
- package/src/adapters/latex/index.test.ts +91 -0
- package/src/adapters/latex/index.ts +3 -2
- package/src/adapters/latex/prose.ts +188 -58
- package/src/cli.ts +19 -18
- package/src/domain/register.test.ts +153 -0
- package/src/domain/register.ts +261 -0
- package/src/domain/tex-document.ts +37 -3
- package/src/ports/latex.ts +6 -0
- package/src/presets.ts +7 -1
- package/src/register-bands.test.ts +327 -0
- package/src/register-bands.ts +291 -0
- package/src/register.ts +7 -37
- package/src/rule-cases.test.ts +67 -17
- package/src/tex-requirements.ts +47 -8
- package/src/tex-venue-rules.ts +7 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE REGISTER OF A PAPER'S BODY, COUNTED — the measures the register rules judge, and the band a
|
|
3
|
+
* venue's accepted papers set for each. Pure: passages and emphasised phrases in, counts out.
|
|
4
|
+
*
|
|
5
|
+
* Three measures, each per 10,000 words of the body, each read off the same passages and cut into
|
|
6
|
+
* the same sentences as every other rule over the body (`sentences.ts`):
|
|
7
|
+
*
|
|
8
|
+
* contrast_frames a claim set against a rejected alternative: «X, not Y», «not X but Y»,
|
|
9
|
+
* «X rather than Y», «instead of Y», «as opposed to Y»
|
|
10
|
+
* claim_emphasis a phrase in bold INSIDE a sentence that states something: six words or more,
|
|
11
|
+
* or holding a number — not a label opening a paragraph or an item, and not a
|
|
12
|
+
* term of a few words
|
|
13
|
+
* relation_markers a word that names how a sentence relates to the one before: because,
|
|
14
|
+
* therefore, however, in contrast, for example, that is …
|
|
15
|
+
*
|
|
16
|
+
* ── THE BAND IS THE ANCHORS', NOT A CONSTANT ────────────────────────────────────
|
|
17
|
+
* A venue preset records its anchors — accepted papers of the venue (or its parent conference),
|
|
18
|
+
* each with its word count and its count of every measure, as these counters give them. The band
|
|
19
|
+
* of a measure is the anchors' range, widened at each end by two Poisson standard deviations of
|
|
20
|
+
* the anchor standing there: a paper of that anchor's length and register would fall outside it
|
|
21
|
+
* about one time in forty. An anchor with no occurrence is widened as if it had one. Below zero is
|
|
22
|
+
* zero. So every number a message names comes from the preset's anchors, and changing an anchor
|
|
23
|
+
* changes the band.
|
|
24
|
+
*/
|
|
25
|
+
import type { Emphasis, Passage, Span, TextRun } from "./tex-document.ts";
|
|
26
|
+
import { runText, spanIn } from "./tex-document.ts";
|
|
27
|
+
import { CITATION, MATH, REFERENCE, runOf, sentences } from "./sentences.ts";
|
|
28
|
+
|
|
29
|
+
/** The measures a venue's anchors set a band for. */
|
|
30
|
+
export type RegisterMeasureName =
|
|
31
|
+
"contrast_frames" | "claim_emphasis" | "relation_markers";
|
|
32
|
+
|
|
33
|
+
export const REGISTER_MEASURES: readonly RegisterMeasureName[] = [
|
|
34
|
+
"contrast_frames",
|
|
35
|
+
"claim_emphasis",
|
|
36
|
+
"relation_markers",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** One accepted paper of the venue, as the counters measured it. */
|
|
40
|
+
export interface RegisterAnchor {
|
|
41
|
+
/** What the preset calls it: a directory under `fixtures/accepted-papers/`, or a citation. */
|
|
42
|
+
readonly paper: string;
|
|
43
|
+
readonly words: number;
|
|
44
|
+
readonly counts: Readonly<Record<RegisterMeasureName, number>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** How far the band reaches past the anchors: this many Poisson standard deviations. */
|
|
48
|
+
export const NOISE_SDS = 2;
|
|
49
|
+
|
|
50
|
+
/** The words a rate is taken over. */
|
|
51
|
+
export const PER = 10_000;
|
|
52
|
+
|
|
53
|
+
/** A rate per `PER` words; 0 for a body with no words. */
|
|
54
|
+
export const rateOf = (count: number, words: number): number =>
|
|
55
|
+
words === 0 ? 0 : (PER * count) / words;
|
|
56
|
+
|
|
57
|
+
/** A measure's band, and the anchors' own range it was widened from. */
|
|
58
|
+
export interface Band {
|
|
59
|
+
readonly min: number;
|
|
60
|
+
readonly max: number;
|
|
61
|
+
readonly anchorMin: number;
|
|
62
|
+
readonly anchorMax: number;
|
|
63
|
+
readonly anchors: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One anchor's rate of a measure, and how far one standard deviation of its count reaches. */
|
|
67
|
+
const anchorRate = (a: RegisterAnchor, m: RegisterMeasureName) => {
|
|
68
|
+
const count = a.counts[m];
|
|
69
|
+
return {
|
|
70
|
+
rate: rateOf(count, a.words),
|
|
71
|
+
sd: rateOf(Math.sqrt(Math.max(count, 1)), a.words),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The band the anchors set for a measure; null when there are no anchors. */
|
|
76
|
+
export function bandOf(
|
|
77
|
+
anchors: readonly RegisterAnchor[],
|
|
78
|
+
m: RegisterMeasureName,
|
|
79
|
+
): Band | null {
|
|
80
|
+
const rates = anchors.map((a) => anchorRate(a, m));
|
|
81
|
+
const low = rates.reduce<(typeof rates)[number] | null>(
|
|
82
|
+
(lo, r) => (lo === null || r.rate < lo.rate ? r : lo),
|
|
83
|
+
null,
|
|
84
|
+
);
|
|
85
|
+
const high = rates.reduce<(typeof rates)[number] | null>(
|
|
86
|
+
(hi, r) => (hi === null || r.rate > hi.rate ? r : hi),
|
|
87
|
+
null,
|
|
88
|
+
);
|
|
89
|
+
if (low === null || high === null) return null;
|
|
90
|
+
return {
|
|
91
|
+
min: Math.max(0, low.rate - NOISE_SDS * low.sd),
|
|
92
|
+
max: high.rate + NOISE_SDS * high.sd,
|
|
93
|
+
anchorMin: low.rate,
|
|
94
|
+
anchorMax: high.rate,
|
|
95
|
+
anchors: anchors.length,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── sentences, as every register measure reads them ───────────────────────────────
|
|
100
|
+
|
|
101
|
+
/** The characters a mark or math stands as in a sentence's text. */
|
|
102
|
+
const MARK = new RegExp(`[${CITATION}${REFERENCE}${MATH}]`, "gu");
|
|
103
|
+
|
|
104
|
+
const WORDS = new Intl.Segmenter("en", { granularity: "word" });
|
|
105
|
+
|
|
106
|
+
/** A sentence of the body: its text as the reader sees it, its words, its first word, where it stands. */
|
|
107
|
+
export interface BodySentence {
|
|
108
|
+
/** The text with marks and math left out and spaces collapsed, for a message. */
|
|
109
|
+
readonly text: string;
|
|
110
|
+
/** The text with each mark and math as one character, for matching. */
|
|
111
|
+
readonly raw: string;
|
|
112
|
+
/** Its word-like words, and one for each mark or math, as it reads on the page. */
|
|
113
|
+
readonly words: number;
|
|
114
|
+
readonly first: string;
|
|
115
|
+
/** The run it was cut from, and where in that run's text it starts. */
|
|
116
|
+
readonly run: TextRun;
|
|
117
|
+
readonly from: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The body's sentences that hold a word, in order. */
|
|
121
|
+
export function bodySentences(
|
|
122
|
+
passages: readonly Passage[],
|
|
123
|
+
): readonly BodySentence[] {
|
|
124
|
+
return passages.flatMap((p) => {
|
|
125
|
+
const run = runOf(p);
|
|
126
|
+
const text = runText(run);
|
|
127
|
+
return sentences(text).flatMap(([from, to]) => {
|
|
128
|
+
const raw = text.slice(from, to);
|
|
129
|
+
const words = [...WORDS.segment(raw)].filter((w) => w.isWordLike);
|
|
130
|
+
const [first] = words;
|
|
131
|
+
if (first === undefined) return [];
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
text: raw.replace(MARK, "").replace(/\s+/gu, " ").trim(),
|
|
135
|
+
raw,
|
|
136
|
+
words: words.length + (raw.match(MARK)?.length ?? 0),
|
|
137
|
+
first: first.segment,
|
|
138
|
+
run,
|
|
139
|
+
from,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The body's words. */
|
|
147
|
+
export const wordsOf = (ss: readonly BodySentence[]): number =>
|
|
148
|
+
ss.reduce((n, s) => n + s.words, 0);
|
|
149
|
+
|
|
150
|
+
// ── the measures ──────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One occurrence of a measure: the words around it as the reader sees them, the sentence it stands
|
|
154
|
+
* in, and where it is.
|
|
155
|
+
*/
|
|
156
|
+
export interface Occurrence {
|
|
157
|
+
readonly match: string;
|
|
158
|
+
readonly sentence: string;
|
|
159
|
+
readonly at: Span | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The forms as one pattern: matched left to right, so one phrase is never two occurrences. */
|
|
163
|
+
const anyOf = (forms: readonly RegExp[]): Readonly<RegExp> =>
|
|
164
|
+
new RegExp(forms.map((r) => `(?:${r.source})`).join("|"), "giu");
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* «not» after a comma, a dash (`---` as the source spells it) or a semicolon — «the word, not the
|
|
168
|
+
* operation», «--- not authenticity» —
|
|
169
|
+
* unless it opens a phrase that rejects nothing («not only», «not yet», «not already in the corpus»).
|
|
170
|
+
*/
|
|
171
|
+
const COMMA_NOT =
|
|
172
|
+
/(?:[,;—–]|--)\s*(?:and\s+|but\s+)?not\s+(?!(?:only|least|yet|always|necessarily|all|even|just|merely|surprisingly|already|to)\b)/giu;
|
|
173
|
+
|
|
174
|
+
/** «not X but Y» inside one clause — no comma, colon or semicolon between — but not «not only … but also». */
|
|
175
|
+
const NOT_BUT = /\bnot\b(?!\s+only\b)[^.;:,]{1,60}?\bbut\b(?!\s+also\b)/giu;
|
|
176
|
+
|
|
177
|
+
/** Every form of a contrast frame, as one pattern over one sentence. */
|
|
178
|
+
const CONTRAST_FRAME = anyOf([
|
|
179
|
+
COMMA_NOT,
|
|
180
|
+
NOT_BUT,
|
|
181
|
+
/\brather\s+than\b/u,
|
|
182
|
+
/\binstead\s+of\b/u,
|
|
183
|
+
/\bas\s+opposed\s+to\b/u,
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Words that name the relation between a sentence and the one before it: cause, consequence,
|
|
188
|
+
* contrast, concession, example, restatement. The list is the one the register diagnosis measured
|
|
189
|
+
* (every marker whose meaning does not depend on context), not tuned to this corpus.
|
|
190
|
+
*/
|
|
191
|
+
const RELATION_MARKERS =
|
|
192
|
+
/(?<![\p{L}])(?:therefore|thus|hence|consequently|as a result|because|since|this means|this implies|which explains|in turn|that is,|i\.e\.|in contrast|by contrast|however|nevertheless|nonetheless|whereas|in other words|for this reason|it follows|accordingly|specifically,|in particular,|for example|for instance|e\.g\.)(?![\p{L}])/giu;
|
|
193
|
+
|
|
194
|
+
/** How many characters of context an excerpt keeps on each side of a match. */
|
|
195
|
+
const CONTEXT = 40;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The words around `[from, to)` of a sentence as the reader sees them: the match with up to
|
|
199
|
+
* `CONTEXT` characters on each side, cut at a word, marks and math left out.
|
|
200
|
+
*/
|
|
201
|
+
export function excerpt(raw: string, from: number, to: number): string {
|
|
202
|
+
const start = Math.max(0, from - CONTEXT);
|
|
203
|
+
const end = Math.min(raw.length, to + CONTEXT);
|
|
204
|
+
const words = raw
|
|
205
|
+
.slice(start, end)
|
|
206
|
+
.replace(MARK, "")
|
|
207
|
+
.split(/\s+/u)
|
|
208
|
+
.filter((w) => w !== "");
|
|
209
|
+
const inner = words.slice(
|
|
210
|
+
start > 0 ? 1 : 0,
|
|
211
|
+
end < raw.length ? -1 : words.length,
|
|
212
|
+
);
|
|
213
|
+
return `${start > 0 ? "…" : ""}${inner.join(" ")}${end < raw.length ? "…" : ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Every match of `pattern` in every sentence, in order, with where it stands. */
|
|
217
|
+
function occurrences(
|
|
218
|
+
ss: readonly BodySentence[],
|
|
219
|
+
pattern: Readonly<RegExp>,
|
|
220
|
+
): readonly Occurrence[] {
|
|
221
|
+
return ss.flatMap((s) =>
|
|
222
|
+
[...s.raw.matchAll(pattern)].map((m) => ({
|
|
223
|
+
match: excerpt(s.raw, m.index, m.index + m[0].length),
|
|
224
|
+
sentence: s.text,
|
|
225
|
+
at: spanIn(s.run, s.from + m.index, s.from + m.index + m[0].length),
|
|
226
|
+
})),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The contrast frames of the body's sentences. */
|
|
231
|
+
export const contrastFrames = (
|
|
232
|
+
ss: readonly BodySentence[],
|
|
233
|
+
): readonly Occurrence[] => occurrences(ss, CONTRAST_FRAME);
|
|
234
|
+
|
|
235
|
+
/** The relation markers of the body's sentences. */
|
|
236
|
+
export const relationMarkers = (
|
|
237
|
+
ss: readonly BodySentence[],
|
|
238
|
+
): readonly Occurrence[] => occurrences(ss, RELATION_MARKERS);
|
|
239
|
+
|
|
240
|
+
/** A phrase this long or longer states something rather than names it. */
|
|
241
|
+
export const CLAIM_WORDS = 6;
|
|
242
|
+
|
|
243
|
+
const wordCount = (s: string): number =>
|
|
244
|
+
[...WORDS.segment(s)].filter((w) => w.isWordLike).length;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether an emphasised phrase is a claim set in bold: bold, inside its sentence (not a label that
|
|
248
|
+
* opens a paragraph or an item), and either `CLAIM_WORDS` words long or holding a number.
|
|
249
|
+
*/
|
|
250
|
+
export const isClaimEmphasis = (e: Emphasis): boolean =>
|
|
251
|
+
e.style === "bold" &&
|
|
252
|
+
e.place === "inline" &&
|
|
253
|
+
(wordCount(e.text) >= CLAIM_WORDS || /\p{N}/u.test(e.text));
|
|
254
|
+
|
|
255
|
+
/** The claims the body sets in bold. */
|
|
256
|
+
export const claimEmphasis = (es: readonly Emphasis[]): readonly Occurrence[] =>
|
|
257
|
+
es.filter(isClaimEmphasis).map((e) => ({
|
|
258
|
+
match: e.text,
|
|
259
|
+
sentence: e.text,
|
|
260
|
+
at: e.span,
|
|
261
|
+
}));
|
|
@@ -110,9 +110,21 @@ export const runText = (run: TextRun): string =>
|
|
|
110
110
|
|
|
111
111
|
/** The source offset of the `i`-th character of the text these segments make, or null outside it. */
|
|
112
112
|
function offsetIn(segments: readonly Segment[], i: number): number | null {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
if (i < 0) return null;
|
|
114
|
+
// One pass, no copy of the rest per segment: a paragraph's run holds thousands of segments.
|
|
115
|
+
const found = segments.reduce<{
|
|
116
|
+
readonly rest: number;
|
|
117
|
+
readonly at: number | null;
|
|
118
|
+
}>(
|
|
119
|
+
(acc, s) =>
|
|
120
|
+
acc.at !== null
|
|
121
|
+
? acc
|
|
122
|
+
: acc.rest < s.text.length
|
|
123
|
+
? { rest: acc.rest, at: s.at + acc.rest }
|
|
124
|
+
: { rest: acc.rest - s.text.length, at: null },
|
|
125
|
+
{ rest: i, at: null },
|
|
126
|
+
);
|
|
127
|
+
return found.at;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
/**
|
|
@@ -142,6 +154,28 @@ export interface Passage {
|
|
|
142
154
|
readonly pieces: readonly [ProsePiece, ...ProsePiece[]];
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
/** How an emphasised phrase is set: bold, italic (or `\emph`), or underlined. */
|
|
158
|
+
export type EmphasisStyle = "bold" | "italic" | "underline";
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Where an emphasised phrase stands: opening its paragraph or list item — a label, set like a run-in
|
|
162
|
+
* heading even without the full stop (`\item \textbf{A corpus of questions}: …`) — or inside the
|
|
163
|
+
* running text, after other words of its sentence.
|
|
164
|
+
*/
|
|
165
|
+
export type EmphasisPlace = "opening" | "inline";
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A phrase set in bold, italics or underline inside the body's running prose — not a heading, a
|
|
169
|
+
* run-in heading, a caption, a float or a table, which are never prose (`LatexReader.bodyProse`).
|
|
170
|
+
* `text` is what it typesets, marks and math left out.
|
|
171
|
+
*/
|
|
172
|
+
export interface Emphasis {
|
|
173
|
+
readonly style: EmphasisStyle;
|
|
174
|
+
readonly place: EmphasisPlace;
|
|
175
|
+
readonly text: string;
|
|
176
|
+
readonly span: Span;
|
|
177
|
+
}
|
|
178
|
+
|
|
145
179
|
/**
|
|
146
180
|
* A command that changes the page layout a venue's template sets: the text block, the margins,
|
|
147
181
|
* the line spacing, or space pulled back with a negative skip. `command` is how a message names it
|
package/src/ports/latex.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Include } from "../domain/paper-source.ts";
|
|
|
6
6
|
import type {
|
|
7
7
|
ClassLine,
|
|
8
8
|
DocumentClass,
|
|
9
|
+
Emphasis,
|
|
9
10
|
LayoutOverride,
|
|
10
11
|
Outline,
|
|
11
12
|
Passage,
|
|
@@ -26,6 +27,11 @@ export interface LatexReader {
|
|
|
26
27
|
* code or the title block. Citations, links and cross-references stay in place as marks.
|
|
27
28
|
*/
|
|
28
29
|
readonly bodyProse: (src: string) => readonly Passage[];
|
|
30
|
+
/**
|
|
31
|
+
* The phrases set in bold, italics or underline inside that same prose, in document order — never
|
|
32
|
+
* a heading, a run-in heading (`\textbf{Threats.} …`), a caption, a float or a table.
|
|
33
|
+
*/
|
|
34
|
+
readonly bodyEmphasis: (src: string) => readonly Emphasis[];
|
|
29
35
|
/** Every `\input`, `\include` and `\subfile`, in source order: the path as written, where it stands. */
|
|
30
36
|
readonly includes: (src: string) => readonly Include[];
|
|
31
37
|
/** The span of the `document` environment's body, or null when the source has none. */
|
package/src/presets.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tex union — a child never removes a package its parent needs
|
|
20
20
|
* format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
|
|
21
21
|
* rules per rule id, the child wins
|
|
22
|
-
* template the child wins; so does `name`, and `required_sections` (
|
|
22
|
+
* template the child wins; so does `name`, and `required_sections` and `register` (each whole)
|
|
23
23
|
* aliases union, with every `name` — what the venue is called along the chain
|
|
24
24
|
*
|
|
25
25
|
* ── THE LABEL ─────────────────────────────────────────────────────────────────────
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "./paper-settings.ts";
|
|
52
52
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
53
53
|
import { messageOf } from "./domain/text.ts";
|
|
54
|
+
import type { RegisterAnchor } from "./domain/register.ts";
|
|
54
55
|
|
|
55
56
|
/** The prefix of a shipped preset's spec. */
|
|
56
57
|
export const SHIPPED_PREFIX = "paperlint:";
|
|
@@ -89,6 +90,8 @@ export interface Preset {
|
|
|
89
90
|
readonly tex: TexRequirements;
|
|
90
91
|
readonly format: VenueFormat;
|
|
91
92
|
readonly rules: Readonly<Record<string, unknown>>;
|
|
93
|
+
/** The accepted papers the register rules measure a body against; empty when no file names any. */
|
|
94
|
+
readonly registerAnchors: readonly RegisterAnchor[];
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/** Why a spec does not resolve. */
|
|
@@ -219,6 +222,7 @@ function merged(
|
|
|
219
222
|
tex: NO_REQUIREMENTS,
|
|
220
223
|
format: NO_FORMAT,
|
|
221
224
|
rules: {},
|
|
225
|
+
registerAnchors: [],
|
|
222
226
|
};
|
|
223
227
|
const m = rootFirst.reduce(
|
|
224
228
|
(acc, { preset: p, file }) => ({
|
|
@@ -236,6 +240,7 @@ function merged(
|
|
|
236
240
|
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
237
241
|
format: mergeFormat(acc.format, p.format),
|
|
238
242
|
rules: { ...acc.rules, ...p.rules },
|
|
243
|
+
registerAnchors: p.registerAnchors ?? acc.registerAnchors,
|
|
239
244
|
}),
|
|
240
245
|
base,
|
|
241
246
|
);
|
|
@@ -249,6 +254,7 @@ function merged(
|
|
|
249
254
|
tex: m.tex,
|
|
250
255
|
format: m.format,
|
|
251
256
|
rules: m.rules,
|
|
257
|
+
registerAnchors: m.registerAnchors,
|
|
252
258
|
};
|
|
253
259
|
}
|
|
254
260
|
|