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
package/src/cli.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* but that is INTERNAL machinery, and you no longer need to know it in order to run them.
|
|
10
10
|
*
|
|
11
11
|
* Two entry points into the tool, and both are whole now:
|
|
12
|
-
* npx paperlint lint
|
|
13
|
-
* uses:
|
|
12
|
+
* npx paperlint lint ← here
|
|
13
|
+
* uses: ./node_modules/paperlint ← action.yml, after `npm ci`
|
|
14
14
|
*
|
|
15
15
|
* ⚠️ THE BOUNDARY THIS UTILITY HAS NO RIGHT TO ERASE: the consumer's data stays with the consumer.
|
|
16
16
|
* Where the papers are and the project's own rule blocks — that is about ONE corpus, so it lives in
|
|
@@ -42,19 +42,32 @@ import {
|
|
|
42
42
|
asEslintResults,
|
|
43
43
|
} from "./structure.ts";
|
|
44
44
|
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
45
|
+
import { includeBlocks, paperBodies, unreadLines } from "./paper-includes.ts";
|
|
45
46
|
import { prepareEngine } from "./build-engine.ts";
|
|
46
47
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
47
48
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
48
49
|
import { curlDownload } from "./adapters/curl/index.ts";
|
|
49
50
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
50
51
|
import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
|
|
51
|
-
import {
|
|
52
|
-
|
|
52
|
+
import {
|
|
53
|
+
ANONYMITY_RULE_LEVELS,
|
|
54
|
+
anonymityRules,
|
|
55
|
+
PAGE_LIMIT_RULE_LEVELS,
|
|
56
|
+
pageLimitRules,
|
|
57
|
+
VENUE_RULE_LEVELS,
|
|
58
|
+
venueRules,
|
|
59
|
+
} from "./venue-rules.ts";
|
|
60
|
+
import {
|
|
61
|
+
FORMAT_RULE_LEVELS,
|
|
62
|
+
formatRules,
|
|
63
|
+
TEX_VENUE_RULE_LEVELS,
|
|
64
|
+
texVenueRules,
|
|
65
|
+
} from "./tex-venue-rules.ts";
|
|
53
66
|
import { claimProvenanceRule } from "./claim-provenance.ts";
|
|
54
67
|
import { registerRule } from "./register.ts";
|
|
55
|
-
import { missingInputRule } from "./tex-paper.ts";
|
|
56
68
|
import {
|
|
57
69
|
paperRules,
|
|
70
|
+
identityField,
|
|
58
71
|
stringFields,
|
|
59
72
|
type PaperSettings,
|
|
60
73
|
} from "./paper-settings.ts";
|
|
@@ -125,7 +138,6 @@ export { init };
|
|
|
125
138
|
export { nextSteps } from "./init.ts";
|
|
126
139
|
|
|
127
140
|
import paperStages from "#eslint-rules/paper-stages";
|
|
128
|
-
import researchQuestion from "#eslint-rules/paper-research-question";
|
|
129
141
|
import typography from "#eslint-rules/paper-typography";
|
|
130
142
|
import texBuild from "#eslint-rules/tex-build";
|
|
131
143
|
import bibReachable from "#eslint-rules/bib-reachable-entry";
|
|
@@ -219,8 +231,9 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
219
231
|
blocks (files, ignores, rules) with globs relative to that file. Order, later wins: paperlint's
|
|
220
232
|
own, the venue preset's, the root file's, the paper's. Optional rules (off unless turned on):
|
|
221
233
|
pdf/last-page-balance. The venue rules (pdf/fresh, pdf/profile, pdf/fonts, pdf/geometry,
|
|
222
|
-
pdf/
|
|
223
|
-
"off" to skip it. An unknown key, in either
|
|
234
|
+
pdf/body-size, pdf/measured, format/page-limit, format/layout-override, anonymity/identity) are
|
|
235
|
+
on for every paper with a venue preset; set one to "off" to skip it. An unknown key, in either
|
|
236
|
+
file, is an error.
|
|
224
237
|
`;
|
|
225
238
|
|
|
226
239
|
/** The config the user would otherwise write by hand. The data comes from `opts`, the mechanism is here. */
|
|
@@ -228,7 +241,7 @@ export function buildConfig(
|
|
|
228
241
|
opts: PaperlintConfig = {},
|
|
229
242
|
texLanguage: unknown,
|
|
230
243
|
): ConfigBlock[] {
|
|
231
|
-
const paperRules = { ...
|
|
244
|
+
const paperRules = { ...typography.rules };
|
|
232
245
|
// The reference rules judge `_build/references.json`, and only on `paper.tex`.
|
|
233
246
|
const texPaperRules = {
|
|
234
247
|
...paperRules,
|
|
@@ -236,7 +249,6 @@ export function buildConfig(
|
|
|
236
249
|
};
|
|
237
250
|
// Each typography rule reports every occurrence where it is, and fixes it (`--fix`).
|
|
238
251
|
const prose = {
|
|
239
|
-
"paper/research-question": "warn",
|
|
240
252
|
"paper/section-word": "warn",
|
|
241
253
|
"paper/leading-zero": "warn",
|
|
242
254
|
};
|
|
@@ -264,6 +276,9 @@ export function buildConfig(
|
|
|
264
276
|
...venueRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
265
277
|
},
|
|
266
278
|
},
|
|
279
|
+
anonymity: {
|
|
280
|
+
rules: anonymityRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
281
|
+
},
|
|
267
282
|
},
|
|
268
283
|
},
|
|
269
284
|
{
|
|
@@ -319,10 +334,6 @@ export function buildConfig(
|
|
|
319
334
|
files: nodeFiles,
|
|
320
335
|
latex: latexReader,
|
|
321
336
|
}),
|
|
322
|
-
"missing-input": missingInputRule({
|
|
323
|
-
files: nodeFiles,
|
|
324
|
-
latex: latexReader,
|
|
325
|
-
}),
|
|
326
337
|
register: registerRule({
|
|
327
338
|
files: nodeFiles,
|
|
328
339
|
latex: latexReader,
|
|
@@ -331,6 +342,16 @@ export function buildConfig(
|
|
|
331
342
|
},
|
|
332
343
|
paper: { rules: texPaperRules },
|
|
333
344
|
bib: bibReachable,
|
|
345
|
+
format: {
|
|
346
|
+
rules: {
|
|
347
|
+
...formatRules({
|
|
348
|
+
files: nodeFiles,
|
|
349
|
+
venuesDir: presetsDir(),
|
|
350
|
+
latex: latexReader,
|
|
351
|
+
}),
|
|
352
|
+
...pageLimitRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
334
355
|
},
|
|
335
356
|
language: "tex/latex",
|
|
336
357
|
rules: {
|
|
@@ -341,26 +362,65 @@ export function buildConfig(
|
|
|
341
362
|
"tex/future-promise": "warn",
|
|
342
363
|
// A number with no owner: a prose rule, on beside the others above.
|
|
343
364
|
"tex/claim-provenance": "warn",
|
|
344
|
-
"tex/missing-input": "warn",
|
|
345
365
|
// Sentence-initial conjunctions over the whole body: register, not vocabulary.
|
|
346
366
|
"tex/register": "warn",
|
|
347
367
|
"tex/acm-frontmatter-override": "error",
|
|
348
368
|
// Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
|
|
349
369
|
...VENUE_RULE_LEVELS,
|
|
370
|
+
...ANONYMITY_RULE_LEVELS,
|
|
350
371
|
...TEX_VENUE_RULE_LEVELS,
|
|
372
|
+
...FORMAT_RULE_LEVELS,
|
|
373
|
+
...PAGE_LIMIT_RULE_LEVELS,
|
|
351
374
|
},
|
|
352
375
|
});
|
|
376
|
+
// THE FILES A paper.tex INCLUDES FROM ITS BODY, each read on its own and reported at its own path
|
|
377
|
+
// (#144). The block claims every `.tex` but `paper.tex` — yet it is left out of the scope below, so
|
|
378
|
+
// it reaches only the files `paperlint lint` un-ignores by name for each paper (src/paper-includes.ts):
|
|
379
|
+
// a frozen version or a macro file is never enumerated. The rules are the ones that read a file as
|
|
380
|
+
// a fragment (no `document` environment: all of it is body); the whole-paper rules stay on
|
|
381
|
+
// `paper.tex`, which reads its includes itself (src/tex-paper.ts).
|
|
382
|
+
const fragments: readonly ConfigBlock[] = texLanguage
|
|
383
|
+
? [
|
|
384
|
+
{
|
|
385
|
+
files: ["**/*.tex"],
|
|
386
|
+
ignores: ["**/paper.tex"],
|
|
387
|
+
plugins: {
|
|
388
|
+
tex: {
|
|
389
|
+
languages: { latex: texLanguage },
|
|
390
|
+
rules: { "future-promise": texBuild["future-promise"] },
|
|
391
|
+
},
|
|
392
|
+
paper: {
|
|
393
|
+
rules: {
|
|
394
|
+
"section-word": typography.rules["section-word"],
|
|
395
|
+
"leading-zero": typography.rules["leading-zero"],
|
|
396
|
+
"figure-ref-style": typography.rules["figure-ref-style"],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
language: "tex/latex",
|
|
401
|
+
rules: {
|
|
402
|
+
"paper/section-word": "warn",
|
|
403
|
+
"paper/leading-zero": "warn",
|
|
404
|
+
"paper/figure-ref-style": "warn",
|
|
405
|
+
"tex/future-promise": "warn",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
]
|
|
409
|
+
: [];
|
|
353
410
|
// 🔴 ONLY THE FILES THESE BLOCKS CLAIM ARE LINTED (src/paper-files.ts). Without this block ESLint's
|
|
354
411
|
// built-in defaults lint every .js/.mjs/.cjs under the papers directory — a paper's vendored
|
|
355
412
|
// `repro/` code failed the run with 74 parse errors and not one finding on a paper file. It goes
|
|
356
413
|
// FIRST: the `.template/` ignore below must come after its directory un-ignore to win.
|
|
357
|
-
const owners = ruleOwners(cfg);
|
|
414
|
+
const owners = ruleOwners([...cfg, ...fragments]);
|
|
358
415
|
cfg.unshift(scopeToOwned(ownedPatterns(cfg)));
|
|
359
416
|
// The consumer's own blocks, LAST, so a later block wins — ESLint's rule. Parsed by
|
|
360
417
|
// `readConfig`; each carries the settings file's directory as its `basePath`. Each is split so a
|
|
361
418
|
// rule reaches only the files its plugin is registered for: `paper` is a different plugin beside
|
|
362
419
|
// PIPELINE-STATUS.md than beside paper.tex, and ESLint throws on a rule its plugin lacks.
|
|
363
|
-
cfg.push(
|
|
420
|
+
cfg.push(
|
|
421
|
+
...fragments,
|
|
422
|
+
...(opts.rules ?? []).flatMap((b) => narrowToOwners(b, owners)),
|
|
423
|
+
);
|
|
364
424
|
return cfg;
|
|
365
425
|
}
|
|
366
426
|
|
|
@@ -501,6 +561,17 @@ export async function silentOptionalRules(
|
|
|
501
561
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
502
562
|
}
|
|
503
563
|
|
|
564
|
+
/**
|
|
565
|
+
* The paper directories `paths` reach: each directory and the papers under it. A FILE named on the
|
|
566
|
+
* command line belongs to the paper it sits in.
|
|
567
|
+
*/
|
|
568
|
+
function paperDirs(paths: readonly string[]): readonly string[] {
|
|
569
|
+
const dirs = paths.map((p) =>
|
|
570
|
+
existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
|
|
571
|
+
);
|
|
572
|
+
return [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
|
|
573
|
+
}
|
|
574
|
+
|
|
504
575
|
/**
|
|
505
576
|
* Every linted paper's rules, as ESLint blocks scoped to that paper, in two groups: what its venue
|
|
506
577
|
* preset turns on, and what its own `paperlint.json` says. The caller puts the root's blocks between
|
|
@@ -510,11 +581,7 @@ export async function silentOptionalRules(
|
|
|
510
581
|
export function paperRuleBlocks(
|
|
511
582
|
paths: readonly string[],
|
|
512
583
|
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
513
|
-
|
|
514
|
-
const dirs = paths.map((p) =>
|
|
515
|
-
existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
|
|
516
|
-
);
|
|
517
|
-
const papers = [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
|
|
584
|
+
const papers = paperDirs(paths);
|
|
518
585
|
const out: { preset: RuleBlock[]; own: RuleBlock[] } = {
|
|
519
586
|
preset: [],
|
|
520
587
|
own: [],
|
|
@@ -630,6 +697,8 @@ export function parseSettings(
|
|
|
630
697
|
};
|
|
631
698
|
const defaults = stringFields(raw);
|
|
632
699
|
if (!defaults.ok) return { ok: false, error: `${where}: ${defaults.error}` };
|
|
700
|
+
const identity = identityField(raw);
|
|
701
|
+
if (!identity.ok) return { ok: false, error: `${where}: ${identity.error}` };
|
|
633
702
|
const structure = Structure.safeParse(raw["structure"]);
|
|
634
703
|
if (!structure.success)
|
|
635
704
|
return {
|
|
@@ -765,28 +834,6 @@ const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
|
765
834
|
"--max-warnings",
|
|
766
835
|
]);
|
|
767
836
|
|
|
768
|
-
/**
|
|
769
|
-
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
770
|
-
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
771
|
-
* the placeholder instead of guessing.
|
|
772
|
-
*/
|
|
773
|
-
export function ownVersion(
|
|
774
|
-
readManifest: () => string = () =>
|
|
775
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
776
|
-
): string | undefined {
|
|
777
|
-
try {
|
|
778
|
-
const manifest: unknown = JSON.parse(readManifest());
|
|
779
|
-
return typeof manifest === "object" &&
|
|
780
|
-
manifest !== null &&
|
|
781
|
-
"version" in manifest &&
|
|
782
|
-
typeof manifest.version === "string"
|
|
783
|
-
? manifest.version
|
|
784
|
-
: undefined;
|
|
785
|
-
} catch {
|
|
786
|
-
return undefined;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
837
|
/**
|
|
791
838
|
* Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
|
|
792
839
|
* command needed the same config (`build`): two copies of this block would have drifted apart on
|
|
@@ -1421,7 +1468,6 @@ async function runInit(
|
|
|
1421
1468
|
log,
|
|
1422
1469
|
err,
|
|
1423
1470
|
cwd,
|
|
1424
|
-
version: ownVersion(),
|
|
1425
1471
|
yes: a.yes,
|
|
1426
1472
|
hooks: !a.noHooks,
|
|
1427
1473
|
paper: a.paper,
|
|
@@ -1557,10 +1603,32 @@ export async function run(
|
|
|
1557
1603
|
texLanguage = null;
|
|
1558
1604
|
}
|
|
1559
1605
|
|
|
1606
|
+
// Each paper's body files, resolved the way the build resolves them — only when the LaTeX language
|
|
1607
|
+
// loaded, since only its rules read them. The project's papers too: `paperlint lint .` reaches
|
|
1608
|
+
// papers two levels down, which a directory's immediate papers do not.
|
|
1609
|
+
const bodies = texLanguage
|
|
1610
|
+
? paperBodies(paperDirs([...paths, ...papersRoots(cfg)]), {
|
|
1611
|
+
files: nodeFiles,
|
|
1612
|
+
latex: latexReader,
|
|
1613
|
+
})
|
|
1614
|
+
: [];
|
|
1615
|
+
// Named only for the papers this run lints: a paper elsewhere in the project is not this run's.
|
|
1616
|
+
const inScope = (dir: string) =>
|
|
1617
|
+
paths.some((p) => dir === p || dir.startsWith(`${p}${sep}`));
|
|
1618
|
+
unreadLines(
|
|
1619
|
+
bodies.filter((b) => inScope(b.dir)),
|
|
1620
|
+
(p) => shown(cwd, p),
|
|
1621
|
+
).forEach((l) => {
|
|
1622
|
+
err(l);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1560
1625
|
const eslint = new ESLint({
|
|
1561
1626
|
cwd: lintRoot(root, paths),
|
|
1562
1627
|
overrideConfigFile: true,
|
|
1563
|
-
overrideConfig: eslintConfig(
|
|
1628
|
+
overrideConfig: eslintConfig([
|
|
1629
|
+
...buildConfig(withPapers, texLanguage),
|
|
1630
|
+
...includeBlocks(bodies),
|
|
1631
|
+
]),
|
|
1564
1632
|
fix: a.fix,
|
|
1565
1633
|
});
|
|
1566
1634
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
findLeaks,
|
|
4
|
+
spellingsOf,
|
|
5
|
+
tokenPattern,
|
|
6
|
+
type Searchable,
|
|
7
|
+
} from "./anonymity.ts";
|
|
8
|
+
|
|
9
|
+
const page = (text: string, n = 1): Searchable => ({
|
|
10
|
+
place: { kind: "page", page: n },
|
|
11
|
+
text,
|
|
12
|
+
});
|
|
13
|
+
const found = (token: string, text: string): string[] =>
|
|
14
|
+
findLeaks([token], [page(text)]).map((l) => l.found);
|
|
15
|
+
|
|
16
|
+
describe("findLeaks — the spellings of one name a PDF produces", () => {
|
|
17
|
+
it("plain, and in any case (the flags fold Unicode, no locale)", () => {
|
|
18
|
+
expect(found("Ada Example", "by Ada Example, 2026")).toEqual([
|
|
19
|
+
"Ada Example",
|
|
20
|
+
]);
|
|
21
|
+
expect(found("Ada Example", "ADA EXAMPLE")).toEqual(["ADA EXAMPLE"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("Cyrillic folds too", () => {
|
|
25
|
+
expect(found("Ада Пример", "АДА ПРИМЕР пишет")).toEqual(["АДА ПРИМЕР"]);
|
|
26
|
+
expect(found("АДА", "ада")).toEqual(["ада"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("items joined without a space where kerning split a word", () => {
|
|
30
|
+
expect(found("Ada Example", "AdaExample")).toEqual(["AdaExample"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("letter-spaced text", () => {
|
|
34
|
+
expect(found("Ada Example", "A d a E x a m p l e")).toEqual([
|
|
35
|
+
"A d a E x a m p l e",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("a word hyphenated across a line break, and a name broken across lines", () => {
|
|
40
|
+
expect(found("Ada Example", "Ada Exam-\nple wrote")).toEqual([
|
|
41
|
+
"Ada Exam-\nple",
|
|
42
|
+
]);
|
|
43
|
+
expect(found("Ada Example", "by Ada\nExample")).toEqual(["Ada\nExample"]);
|
|
44
|
+
expect(found("adaexample", "adaexample")).toEqual(["adaexample"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("ligatures (NFKC on both sides)", () => {
|
|
48
|
+
expect(found("Fiona", "fiona")).toEqual(["fiona"]);
|
|
49
|
+
expect(found("fiona", "Fiona")).toEqual(["Fiona"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles and addresses, as whole tokens", () => {
|
|
53
|
+
expect(found("adaexample", "https://github.com/adaexample/tool")).toEqual([
|
|
54
|
+
"adaexample",
|
|
55
|
+
]);
|
|
56
|
+
expect(found("ada@example.org", "mail ada@example.org.")).toEqual([
|
|
57
|
+
"ada@example.org",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not match inside another word", () => {
|
|
62
|
+
expect(found("Ada", "Canada and adamant")).toEqual([]);
|
|
63
|
+
expect(found("MIT", "submitted")).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("says nothing about a clean text", () => {
|
|
67
|
+
expect(found("Ada Example", "Anonymous Authors")).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("findLeaks — places and tokens", () => {
|
|
72
|
+
it("reports each token once per place, with the place", () => {
|
|
73
|
+
const leaks = findLeaks(
|
|
74
|
+
["Ada Example", "ada@example.org"],
|
|
75
|
+
[
|
|
76
|
+
page("Ada Example and Ada Example", 2),
|
|
77
|
+
{
|
|
78
|
+
place: { kind: "metadata", field: "Author" },
|
|
79
|
+
text: "ada@example.org",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
place: {
|
|
83
|
+
kind: "link",
|
|
84
|
+
page: 3,
|
|
85
|
+
uri: "https://github.com/adaexample",
|
|
86
|
+
},
|
|
87
|
+
text: "https://github.com/adaexample",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
expect(leaks).toEqual([
|
|
92
|
+
{
|
|
93
|
+
token: "Ada Example",
|
|
94
|
+
place: { kind: "page", page: 2 },
|
|
95
|
+
found: "Ada Example",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
token: "ada@example.org",
|
|
99
|
+
place: { kind: "metadata", field: "Author" },
|
|
100
|
+
found: "ada@example.org",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
token: "Ada Example",
|
|
104
|
+
place: { kind: "link", page: 3, uri: "https://github.com/adaexample" },
|
|
105
|
+
found: "adaexample",
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("a token of separators only has no pattern and finds nothing", () => {
|
|
111
|
+
expect(tokenPattern(" - ")).toBeNull();
|
|
112
|
+
expect(findLeaks([" "], [page("anything at all")])).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("findLeaks — one text, two tokens", () => {
|
|
117
|
+
it("is one leak, under the first token declared", () => {
|
|
118
|
+
expect(
|
|
119
|
+
findLeaks(["Ada Example", "adaexample"], [page("by AdaExample")]),
|
|
120
|
+
).toEqual([
|
|
121
|
+
{
|
|
122
|
+
token: "Ada Example",
|
|
123
|
+
place: { kind: "page", page: 1 },
|
|
124
|
+
found: "AdaExample",
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("spellingsOf — a personal name as a reference list prints it", () => {
|
|
131
|
+
it("adds initials and surname-first forms to a name of two to four words", () => {
|
|
132
|
+
expect(spellingsOf("Ada Example")).toEqual([
|
|
133
|
+
"Ada Example",
|
|
134
|
+
"A. Example",
|
|
135
|
+
"Example, Ada",
|
|
136
|
+
"Example, A.",
|
|
137
|
+
]);
|
|
138
|
+
expect(spellingsOf("Ada Byron Example")).toEqual([
|
|
139
|
+
"Ada Byron Example",
|
|
140
|
+
"A. B. Example",
|
|
141
|
+
"Example, Ada Byron",
|
|
142
|
+
"Example, A. B.",
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("leaves a handle, an address, one word, or lowercase words alone", () => {
|
|
147
|
+
expect(spellingsOf("adaexample")).toEqual(["adaexample"]);
|
|
148
|
+
expect(spellingsOf("ada@example.org")).toEqual(["ada@example.org"]);
|
|
149
|
+
expect(spellingsOf("Example")).toEqual(["Example"]);
|
|
150
|
+
expect(spellingsOf("example tool")).toEqual(["example tool"]);
|
|
151
|
+
expect(spellingsOf("A B C D E")).toEqual(["A B C D E"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("a self-citation's author list finds the declared name", () => {
|
|
155
|
+
expect(
|
|
156
|
+
found("Ada Example", "[3] A. Example and B. Other, “A tool,” 2025."),
|
|
157
|
+
).toEqual(["A. Example"]);
|
|
158
|
+
expect(found("Ada Example", "Example, A., and Other, B. (2025)")).toEqual([
|
|
159
|
+
"Example, A.",
|
|
160
|
+
]);
|
|
161
|
+
expect(found("Ada Example", "B. Example wrote")).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHO WROTE THIS PAPER, FOUND IN THE PAPER — the matching `anonymity/identity` judges a blind submission
|
|
3
|
+
* by. A rule cannot know who the author is, so the author DECLARES it (`identity` in
|
|
4
|
+
* `paperlint.json`); given that list, finding it is mechanical, and this module is the mechanism.
|
|
5
|
+
*
|
|
6
|
+
* ── WHAT A MATCH TOLERATES, AND WHY EACH ─────────────────────────────────────────
|
|
7
|
+
* PDF text is not the source text. pdf.js hands back runs of glyphs, and the same name comes out
|
|
8
|
+
* in several spellings of the same letters:
|
|
9
|
+
*
|
|
10
|
+
* ligatures `fi` for `fi` → both sides NFKC-normalized
|
|
11
|
+
* case `ADA EXAMPLE` in small caps → Unicode case folding (the `iu` flags: no
|
|
12
|
+
* locale is consulted, Cyrillic folds too)
|
|
13
|
+
* kerning splits `Ex` `ample` as two items → the page text joins items without a space
|
|
14
|
+
* letter-spacing `A d a E x a m p l e` → whitespace may stand between any two letters
|
|
15
|
+
* line breaks `Ada` ⏎ `Example`, `Exam-` ⏎ `ple` → so may a hyphen and a line break
|
|
16
|
+
* soft hyphens U+00AD inside a word → treated like a hyphen
|
|
17
|
+
* a reference list `A. Example`, `Example, A.` → a personal name's bibliographic forms
|
|
18
|
+
* are searched too (`spellingsOf`)
|
|
19
|
+
*
|
|
20
|
+
* Only the ENDS of a token are anchored: the letter or digit before its first character and after
|
|
21
|
+
* its last must be absent, so `Ada` does not match inside `Canada`. Between its characters anything
|
|
22
|
+
* of the above may stand. The token's own spaces and hyphens are separators too, so
|
|
23
|
+
* `Ada Example` also finds `Ada-Example` and `AdaExample` — the same person.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Where a declared string was found: a page's text, a metadata field, or a link's target. */
|
|
27
|
+
export type LeakPlace =
|
|
28
|
+
| { readonly kind: "page"; readonly page: number }
|
|
29
|
+
| { readonly kind: "metadata"; readonly field: string }
|
|
30
|
+
| { readonly kind: "link"; readonly page: number; readonly uri: string };
|
|
31
|
+
|
|
32
|
+
/** One text to search, and where it came from. */
|
|
33
|
+
export interface Searchable {
|
|
34
|
+
readonly place: LeakPlace;
|
|
35
|
+
readonly text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One declared string found in one place, with the text it matched as it stands there. */
|
|
39
|
+
export interface Leak {
|
|
40
|
+
readonly token: string;
|
|
41
|
+
readonly place: LeakPlace;
|
|
42
|
+
readonly found: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What may stand between two characters of a token: whitespace, hyphens, soft hyphens. */
|
|
46
|
+
const SEPARATOR = "[\\s\\u00AD-]*";
|
|
47
|
+
const BEFORE = "(?<![\\p{L}\\p{N}])";
|
|
48
|
+
const AFTER = "(?![\\p{L}\\p{N}])";
|
|
49
|
+
|
|
50
|
+
/** `c` as a pattern that matches it literally. */
|
|
51
|
+
const literal = (c: string): string =>
|
|
52
|
+
c.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
|
|
53
|
+
|
|
54
|
+
/** The characters of a token that must appear: NFKC, without its own separators. */
|
|
55
|
+
const lettersOf = (token: string): readonly string[] =>
|
|
56
|
+
token
|
|
57
|
+
.normalize("NFKC")
|
|
58
|
+
.replace(/[\s\u00AD-]/gu, "")
|
|
59
|
+
.match(/./gsu) ?? [];
|
|
60
|
+
|
|
61
|
+
/** A word of a personal name: letters, with an apostrophe or a hyphen inside. */
|
|
62
|
+
const NAME_WORD = /^\p{Lu}[\p{L}'’-]*$/u;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The spellings a bibliography gives a personal name, besides the name itself. A token of two to
|
|
66
|
+
* four capitalized words (`Ada Example`, `Ada B. Example` is not one: an initial is not a word)
|
|
67
|
+
* is read as given names and a surname, and the reference list's forms are added: initials
|
|
68
|
+
* (`A. Example`), surname first (`Example, Ada`, `Example, A.`). A token that is not a name — a
|
|
69
|
+
* handle, an address, one word — has only itself.
|
|
70
|
+
*/
|
|
71
|
+
export function spellingsOf(token: string): readonly string[] {
|
|
72
|
+
const words = token.trim().split(/\s+/u);
|
|
73
|
+
if (
|
|
74
|
+
words.length < 2 ||
|
|
75
|
+
words.length > 4 ||
|
|
76
|
+
!words.every((w) => NAME_WORD.test(w))
|
|
77
|
+
)
|
|
78
|
+
return [token];
|
|
79
|
+
const surname = words.slice(-1).join("");
|
|
80
|
+
const given = words.slice(0, -1);
|
|
81
|
+
const initials = given.map((w) => `${w.slice(0, 1)}.`).join(" ");
|
|
82
|
+
return [
|
|
83
|
+
token,
|
|
84
|
+
`${initials} ${surname}`,
|
|
85
|
+
`${surname}, ${given.join(" ")}`,
|
|
86
|
+
`${surname}, ${initials}`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One spelling as a pattern body: its characters, separators allowed between any two. */
|
|
91
|
+
const bodyOf = (spelling: string): string =>
|
|
92
|
+
lettersOf(spelling).map(literal).join(SEPARATOR);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A declared token as the pattern that finds it and its bibliographic spellings (see the module
|
|
96
|
+
* header), or null for a token with nothing but separators in it — which would match everywhere,
|
|
97
|
+
* and is refused where it is read.
|
|
98
|
+
*/
|
|
99
|
+
export function tokenPattern(token: string): Readonly<RegExp> | null {
|
|
100
|
+
if (lettersOf(token).length === 0) return null;
|
|
101
|
+
const bodies = spellingsOf(token).map(bodyOf).join("|");
|
|
102
|
+
return new RegExp(`${BEFORE}(?:${bodies})${AFTER}`, "iu");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Every token found in the sources, once per place: its first match there. Two tokens that match
|
|
107
|
+
* the same text in the same place (`Ada Example` and `adaexample` both finding `AdaExample`) are one
|
|
108
|
+
* leak, reported under the first token declared. Pure.
|
|
109
|
+
*/
|
|
110
|
+
export function findLeaks(
|
|
111
|
+
tokens: readonly string[],
|
|
112
|
+
sources: readonly Searchable[],
|
|
113
|
+
): readonly Leak[] {
|
|
114
|
+
const patterns = tokens.flatMap((token) => {
|
|
115
|
+
const re = tokenPattern(token);
|
|
116
|
+
return re === null ? [] : [{ token, re }];
|
|
117
|
+
});
|
|
118
|
+
return sources.flatMap((s) => {
|
|
119
|
+
const text = s.text.normalize("NFKC");
|
|
120
|
+
const leaks = patterns.flatMap(({ token, re }) => {
|
|
121
|
+
const m = re.exec(text);
|
|
122
|
+
return m === null ? [] : [{ token, place: s.place, found: m[0] }];
|
|
123
|
+
});
|
|
124
|
+
const key = (l: Leak): string => l.found.toLowerCase();
|
|
125
|
+
return leaks.filter(
|
|
126
|
+
(l, i) => leaks.findIndex((x) => key(x) === key(l)) === i,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
}
|