@descent-vtt/spec-brief 0.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/bin/spec-brief.js +19 -0
- package/dist/apply.d.ts +26 -0
- package/dist/apply.js +71 -0
- package/dist/apply.js.map +1 -0
- package/dist/archive.d.ts +81 -0
- package/dist/archive.js +333 -0
- package/dist/archive.js.map +1 -0
- package/dist/brief.d.ts +60 -0
- package/dist/brief.js +152 -0
- package/dist/brief.js.map +1 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +411 -0
- package/dist/cli.js.map +1 -0
- package/dist/collisions.d.ts +50 -0
- package/dist/collisions.js +127 -0
- package/dist/collisions.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.js +353 -0
- package/dist/config.js.map +1 -0
- package/dist/corpus.d.ts +41 -0
- package/dist/corpus.js +154 -0
- package/dist/corpus.js.map +1 -0
- package/dist/engine.d.ts +121 -0
- package/dist/engine.js +276 -0
- package/dist/engine.js.map +1 -0
- package/dist/frontmatter.d.ts +68 -0
- package/dist/frontmatter.js +311 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/fs.d.ts +59 -0
- package/dist/fs.js +189 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +59 -0
- package/dist/git.js +131 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +79 -0
- package/dist/glob.js +465 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +11 -0
- package/dist/integrity.js +20 -0
- package/dist/integrity.js.map +1 -0
- package/dist/links.d.ts +38 -0
- package/dist/links.js +142 -0
- package/dist/links.js.map +1 -0
- package/dist/lint.d.ts +38 -0
- package/dist/lint.js +90 -0
- package/dist/lint.js.map +1 -0
- package/dist/markdown.d.ts +65 -0
- package/dist/markdown.js +274 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugins.d.ts +16 -0
- package/dist/plugins.js +77 -0
- package/dist/plugins.js.map +1 -0
- package/dist/report.d.ts +38 -0
- package/dist/report.js +244 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +58 -0
- package/dist/rules.js +448 -0
- package/dist/rules.js.map +1 -0
- package/dist/scaffold.d.ts +25 -0
- package/dist/scaffold.js +81 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema.d.ts +47 -0
- package/dist/schema.js +195 -0
- package/dist/schema.js.map +1 -0
- package/dist/text.d.ts +40 -0
- package/dist/text.js +95 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
- package/schema.json +321 -0
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relative links, and keeping them true when a file moves.
|
|
3
|
+
*
|
|
4
|
+
* Both repositories this tool was measured against archive a brief by moving
|
|
5
|
+
* it one directory down and then adding `../` to its links by hand. A move
|
|
6
|
+
* that breaks links is a defect the move introduced, so the move repairs them:
|
|
7
|
+
* the file's own relative links, and the links other live briefs hold to it.
|
|
8
|
+
*/
|
|
9
|
+
import { type Scan } from './markdown.js';
|
|
10
|
+
/** A destination that is a path relative to the file holding it. */
|
|
11
|
+
export declare function isRelativeTarget(target: string): boolean;
|
|
12
|
+
/** The path part of a destination and whatever follows it: `?query` or `#fragment`. */
|
|
13
|
+
export declare function splitTarget(target: string): {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly suffix: string;
|
|
16
|
+
};
|
|
17
|
+
/** Normalises a repository-relative path and refuses one that leaves the repository. */
|
|
18
|
+
export declare function normalisePath(path: string): string;
|
|
19
|
+
/** Whether a repository path lies inside a directory. */
|
|
20
|
+
export declare function isInside(path: string, directory: string): boolean;
|
|
21
|
+
export declare function dirOf(path: string): string;
|
|
22
|
+
/** A relative path resolved against a directory, or `null` when it climbs out of the repository. */
|
|
23
|
+
export declare function resolveFrom(directory: string, relative: string): string | null;
|
|
24
|
+
/** The relative path from a directory to a repository path. */
|
|
25
|
+
export declare function relativePath(fromDirectory: string, to: string): string;
|
|
26
|
+
export interface LinkRewrite {
|
|
27
|
+
readonly lines: string[];
|
|
28
|
+
readonly count: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rewrites relative destinations for a file read from `directory` that will
|
|
32
|
+
* be read from `newDirectory`. `retarget` maps the repository path a
|
|
33
|
+
* destination resolves to now onto the one it should resolve to afterwards,
|
|
34
|
+
* which differs only for a file that is itself moving.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rewriteLinks(scanned: Scan, directory: string, newDirectory: string, retarget: (resolved: string) => string): LinkRewrite;
|
|
37
|
+
/** Lines of `scanned` holding a relative link that resolves, from `directory`, to `target`. */
|
|
38
|
+
export declare function linesLinkingTo(scanned: Scan, directory: string, target: string): number[];
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relative links, and keeping them true when a file moves.
|
|
3
|
+
*
|
|
4
|
+
* Both repositories this tool was measured against archive a brief by moving
|
|
5
|
+
* it one directory down and then adding `../` to its links by hand. A move
|
|
6
|
+
* that breaks links is a defect the move introduced, so the move repairs them:
|
|
7
|
+
* the file's own relative links, and the links other live briefs hold to it.
|
|
8
|
+
*/
|
|
9
|
+
import { linksOf } from './markdown.js';
|
|
10
|
+
/** A destination that is a path relative to the file holding it. */
|
|
11
|
+
export function isRelativeTarget(target) {
|
|
12
|
+
if (target === '' || target.startsWith('#') || target.startsWith('/') || target.startsWith('\\'))
|
|
13
|
+
return false;
|
|
14
|
+
return !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(target);
|
|
15
|
+
}
|
|
16
|
+
/** The path part of a destination and whatever follows it: `?query` or `#fragment`. */
|
|
17
|
+
export function splitTarget(target) {
|
|
18
|
+
const cut = target.search(/[?#]/);
|
|
19
|
+
return cut < 0 ? { path: target, suffix: '' } : { path: target.slice(0, cut), suffix: target.slice(cut) };
|
|
20
|
+
}
|
|
21
|
+
/** Normalises a repository-relative path and refuses one that leaves the repository. */
|
|
22
|
+
export function normalisePath(path) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const part of path.replace(/\\/g, '/').split('/')) {
|
|
25
|
+
if (part === '' || part === '.')
|
|
26
|
+
continue;
|
|
27
|
+
if (part === '..') {
|
|
28
|
+
if (out.length === 0)
|
|
29
|
+
throw new Error(`"${path}" is outside the repository`);
|
|
30
|
+
out.pop();
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
out.push(part);
|
|
34
|
+
}
|
|
35
|
+
return out.join('/');
|
|
36
|
+
}
|
|
37
|
+
/** Whether a repository path lies inside a directory. */
|
|
38
|
+
export function isInside(path, directory) {
|
|
39
|
+
const dir = normalisePath(directory);
|
|
40
|
+
return dir === '' || path === dir || path.startsWith(`${dir}/`);
|
|
41
|
+
}
|
|
42
|
+
export function dirOf(path) {
|
|
43
|
+
const slash = path.lastIndexOf('/');
|
|
44
|
+
return slash < 0 ? '' : path.slice(0, slash);
|
|
45
|
+
}
|
|
46
|
+
function parts(path) {
|
|
47
|
+
return path.split('/').filter((p) => p !== '' && p !== '.');
|
|
48
|
+
}
|
|
49
|
+
/** A relative path resolved against a directory, or `null` when it climbs out of the repository. */
|
|
50
|
+
export function resolveFrom(directory, relative) {
|
|
51
|
+
const out = parts(directory);
|
|
52
|
+
for (const part of parts(relative)) {
|
|
53
|
+
if (part === '..') {
|
|
54
|
+
if (out.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
out.pop();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
out.push(part);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out.join('/');
|
|
63
|
+
}
|
|
64
|
+
/** The relative path from a directory to a repository path. */
|
|
65
|
+
export function relativePath(fromDirectory, to) {
|
|
66
|
+
const from = parts(fromDirectory);
|
|
67
|
+
const target = parts(to);
|
|
68
|
+
let common = 0;
|
|
69
|
+
while (common < from.length && from[common] === target[common])
|
|
70
|
+
common += 1;
|
|
71
|
+
const up = from.slice(common).map(() => '..');
|
|
72
|
+
const rest = target.slice(common);
|
|
73
|
+
const joined = [...up, ...rest].join('/');
|
|
74
|
+
return joined === '' ? '.' : joined;
|
|
75
|
+
}
|
|
76
|
+
function decode(path) {
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(path);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Writes a path back in the encoding its original destination used. */
|
|
85
|
+
function encodeLike(original, path) {
|
|
86
|
+
if (!original.includes('%'))
|
|
87
|
+
return path;
|
|
88
|
+
// encodeURIComponent leaves "." and ".." as they are.
|
|
89
|
+
return path.split('/').map(encodeURIComponent).join('/');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Rewrites relative destinations for a file read from `directory` that will
|
|
93
|
+
* be read from `newDirectory`. `retarget` maps the repository path a
|
|
94
|
+
* destination resolves to now onto the one it should resolve to afterwards,
|
|
95
|
+
* which differs only for a file that is itself moving.
|
|
96
|
+
*/
|
|
97
|
+
export function rewriteLinks(scanned, directory, newDirectory, retarget) {
|
|
98
|
+
const lines = [...scanned.lines];
|
|
99
|
+
let count = 0;
|
|
100
|
+
const byLine = new Map();
|
|
101
|
+
for (const link of linksOf(scanned)) {
|
|
102
|
+
if (!isRelativeTarget(link.target))
|
|
103
|
+
continue;
|
|
104
|
+
const { path, suffix } = splitTarget(link.target);
|
|
105
|
+
if (path === '')
|
|
106
|
+
continue;
|
|
107
|
+
const resolved = resolveFrom(directory, decode(path));
|
|
108
|
+
if (resolved === null)
|
|
109
|
+
continue;
|
|
110
|
+
const destination = retarget(resolved);
|
|
111
|
+
// A destination that still resolves where it should keeps its spelling.
|
|
112
|
+
if (resolveFrom(newDirectory, decode(path)) === destination)
|
|
113
|
+
continue;
|
|
114
|
+
const relative = relativePath(newDirectory, destination);
|
|
115
|
+
const trailing = path.endsWith('/') && relative !== '.' ? '/' : '';
|
|
116
|
+
const replacement = `${encodeLike(path, relative)}${trailing}${suffix}`;
|
|
117
|
+
byLine.set(link.line, [...(byLine.get(link.line) ?? []), { start: link.start, end: link.end, replacement }]);
|
|
118
|
+
count += 1;
|
|
119
|
+
}
|
|
120
|
+
for (const [line, edits] of byLine) {
|
|
121
|
+
let text = lines[line];
|
|
122
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
123
|
+
text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);
|
|
124
|
+
}
|
|
125
|
+
lines[line] = text;
|
|
126
|
+
}
|
|
127
|
+
return { lines, count };
|
|
128
|
+
}
|
|
129
|
+
/** Lines of `scanned` holding a relative link that resolves, from `directory`, to `target`. */
|
|
130
|
+
export function linesLinkingTo(scanned, directory, target) {
|
|
131
|
+
const found = new Set();
|
|
132
|
+
for (const link of linksOf(scanned)) {
|
|
133
|
+
if (!isRelativeTarget(link.target))
|
|
134
|
+
continue;
|
|
135
|
+
const { path } = splitTarget(link.target);
|
|
136
|
+
if (path !== '' && resolveFrom(directory, decode(path)) === target)
|
|
137
|
+
found.add(link.line);
|
|
138
|
+
}
|
|
139
|
+
// Lines are scanned in order, so the set is already ascending.
|
|
140
|
+
return [...found];
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Relative links, and keeping them true when a file moves.\n *\n * Both repositories this tool was measured against archive a brief by moving\n * it one directory down and then adding `../` to its links by hand. A move\n * that breaks links is a defect the move introduced, so the move repairs them:\n * the file's own relative links, and the links other live briefs hold to it.\n */\n\nimport { linksOf, type Scan } from './markdown.js';\n\n/** A destination that is a path relative to the file holding it. */\nexport function isRelativeTarget(target: string): boolean {\n if (target === '' || target.startsWith('#') || target.startsWith('/') || target.startsWith('\\\\')) return false;\n return !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(target);\n}\n\n/** The path part of a destination and whatever follows it: `?query` or `#fragment`. */\nexport function splitTarget(target: string): { readonly path: string; readonly suffix: string } {\n const cut = target.search(/[?#]/);\n return cut < 0 ? { path: target, suffix: '' } : { path: target.slice(0, cut), suffix: target.slice(cut) };\n}\n\n/** Normalises a repository-relative path and refuses one that leaves the repository. */\nexport function normalisePath(path: string): string {\n const out: string[] = [];\n for (const part of path.replace(/\\\\/g, '/').split('/')) {\n if (part === '' || part === '.') continue;\n if (part === '..') {\n if (out.length === 0) throw new Error(`\"${path}\" is outside the repository`);\n out.pop();\n continue;\n }\n out.push(part);\n }\n return out.join('/');\n}\n\n/** Whether a repository path lies inside a directory. */\nexport function isInside(path: string, directory: string): boolean {\n const dir = normalisePath(directory);\n return dir === '' || path === dir || path.startsWith(`${dir}/`);\n}\n\nexport function dirOf(path: string): string {\n const slash = path.lastIndexOf('/');\n return slash < 0 ? '' : path.slice(0, slash);\n}\n\nfunction parts(path: string): string[] {\n return path.split('/').filter((p) => p !== '' && p !== '.');\n}\n\n/** A relative path resolved against a directory, or `null` when it climbs out of the repository. */\nexport function resolveFrom(directory: string, relative: string): string | null {\n const out = parts(directory);\n for (const part of parts(relative)) {\n if (part === '..') {\n if (out.length === 0) return null;\n out.pop();\n } else {\n out.push(part);\n }\n }\n return out.join('/');\n}\n\n/** The relative path from a directory to a repository path. */\nexport function relativePath(fromDirectory: string, to: string): string {\n const from = parts(fromDirectory);\n const target = parts(to);\n let common = 0;\n while (common < from.length && from[common] === target[common]) common += 1;\n const up = from.slice(common).map(() => '..');\n const rest = target.slice(common);\n const joined = [...up, ...rest].join('/');\n return joined === '' ? '.' : joined;\n}\n\nfunction decode(path: string): string {\n try {\n return decodeURIComponent(path);\n } catch {\n return path;\n }\n}\n\n/** Writes a path back in the encoding its original destination used. */\nfunction encodeLike(original: string, path: string): string {\n if (!original.includes('%')) return path;\n // encodeURIComponent leaves \".\" and \"..\" as they are.\n return path.split('/').map(encodeURIComponent).join('/');\n}\n\nexport interface LinkRewrite {\n readonly lines: string[];\n readonly count: number;\n}\n\n/**\n * Rewrites relative destinations for a file read from `directory` that will\n * be read from `newDirectory`. `retarget` maps the repository path a\n * destination resolves to now onto the one it should resolve to afterwards,\n * which differs only for a file that is itself moving.\n */\nexport function rewriteLinks(\n scanned: Scan,\n directory: string,\n newDirectory: string,\n retarget: (resolved: string) => string,\n): LinkRewrite {\n const lines = [...scanned.lines];\n let count = 0;\n const byLine = new Map<number, { start: number; end: number; replacement: string }[]>();\n for (const link of linksOf(scanned)) {\n if (!isRelativeTarget(link.target)) continue;\n const { path, suffix } = splitTarget(link.target);\n if (path === '') continue;\n const resolved = resolveFrom(directory, decode(path));\n if (resolved === null) continue;\n const destination = retarget(resolved);\n // A destination that still resolves where it should keeps its spelling.\n if (resolveFrom(newDirectory, decode(path)) === destination) continue;\n const relative = relativePath(newDirectory, destination);\n const trailing = path.endsWith('/') && relative !== '.' ? '/' : '';\n const replacement = `${encodeLike(path, relative)}${trailing}${suffix}`;\n byLine.set(link.line, [...(byLine.get(link.line) ?? []), { start: link.start, end: link.end, replacement }]);\n count += 1;\n }\n for (const [line, edits] of byLine) {\n let text = lines[line] as string;\n for (const edit of [...edits].sort((a, b) => b.start - a.start)) {\n text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);\n }\n lines[line] = text;\n }\n return { lines, count };\n}\n\n/** Lines of `scanned` holding a relative link that resolves, from `directory`, to `target`. */\nexport function linesLinkingTo(scanned: Scan, directory: string, target: string): number[] {\n const found = new Set<number>();\n for (const link of linksOf(scanned)) {\n if (!isRelativeTarget(link.target)) continue;\n const { path } = splitTarget(link.target);\n if (path !== '' && resolveFrom(directory, decode(path)) === target) found.add(link.line);\n }\n // Lines are scanned in order, so the set is already ascending.\n return [...found];\n}\n"]}
|
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running rules: severities from the configuration, plugins beside the
|
|
3
|
+
* built-in rules, findings in a stable order.
|
|
4
|
+
*/
|
|
5
|
+
import type { Brief } from './brief.js';
|
|
6
|
+
import type { Corpus } from './corpus.js';
|
|
7
|
+
import { type Rule } from './rules.js';
|
|
8
|
+
import type { Finding, SeveritySetting } from './types.js';
|
|
9
|
+
export interface Plugin {
|
|
10
|
+
/** Prefixes the plugin's rule ids: `<name>/<rule>`. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly rules: readonly Rule[];
|
|
13
|
+
/** The plugin's entry in the configuration, handed to each rule. */
|
|
14
|
+
readonly options?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface LintOptions {
|
|
17
|
+
readonly plugins?: readonly Plugin[];
|
|
18
|
+
/** Tracked files, for the rules that read the tree. */
|
|
19
|
+
readonly repoFiles?: readonly string[] | null;
|
|
20
|
+
/** Limit the findings to these briefs. The whole corpus is still read. */
|
|
21
|
+
readonly only?: readonly Brief[];
|
|
22
|
+
}
|
|
23
|
+
/** Every rule id a run knows: built-in, collision and plugin. */
|
|
24
|
+
export declare function ruleIds(plugins?: readonly Plugin[]): string[];
|
|
25
|
+
/** The severity configuration assigns a rule, or its own. */
|
|
26
|
+
export declare function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting;
|
|
27
|
+
/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */
|
|
28
|
+
export declare function checkRuleIds(corpus: Corpus, plugins?: readonly Plugin[]): void;
|
|
29
|
+
export declare function lint(corpus: Corpus, options?: LintOptions): Promise<Finding[]>;
|
|
30
|
+
export declare function sortFindings(findings: readonly Finding[]): Finding[];
|
|
31
|
+
export interface Summary {
|
|
32
|
+
readonly errors: number;
|
|
33
|
+
readonly warnings: number;
|
|
34
|
+
readonly notes: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function summarise(findings: readonly Finding[]): Summary;
|
|
37
|
+
/** Whether findings fail a run: any error, or any warning under `--strict`. */
|
|
38
|
+
export declare function failing(findings: readonly Finding[], strict: boolean): boolean;
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running rules: severities from the configuration, plugins beside the
|
|
3
|
+
* built-in rules, findings in a stable order.
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigError } from './config.js';
|
|
6
|
+
import { analyse, COLLISION_RULES, RULES } from './rules.js';
|
|
7
|
+
const RANK = { note: 0, warning: 1, error: 2 };
|
|
8
|
+
function lower(a, b) {
|
|
9
|
+
return RANK[a] <= RANK[b] ? a : b;
|
|
10
|
+
}
|
|
11
|
+
/** Every rule id a run knows: built-in, collision and plugin. */
|
|
12
|
+
export function ruleIds(plugins = []) {
|
|
13
|
+
return [
|
|
14
|
+
...RULES.map((r) => r.id),
|
|
15
|
+
...COLLISION_RULES.map((r) => r.id),
|
|
16
|
+
...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
/** The severity configuration assigns a rule, or its own. */
|
|
20
|
+
export function severityOf(corpus, id, fallback) {
|
|
21
|
+
return corpus.config.rules[id] ?? fallback;
|
|
22
|
+
}
|
|
23
|
+
/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */
|
|
24
|
+
export function checkRuleIds(corpus, plugins = []) {
|
|
25
|
+
const known = new Set(ruleIds(plugins));
|
|
26
|
+
const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));
|
|
27
|
+
if (unknown.length > 0) {
|
|
28
|
+
throw new ConfigError('configuration', unknown.map((id) => `"rules.${id}" names no rule`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function lint(corpus, options = {}) {
|
|
32
|
+
const plugins = options.plugins ?? [];
|
|
33
|
+
checkRuleIds(corpus, plugins);
|
|
34
|
+
const active = [
|
|
35
|
+
...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),
|
|
36
|
+
...plugins.flatMap((plugin) => plugin.rules.map((rule) => {
|
|
37
|
+
const id = `${plugin.name}/${rule.id}`;
|
|
38
|
+
return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };
|
|
39
|
+
})),
|
|
40
|
+
].filter((r) => r.severity !== 'off');
|
|
41
|
+
const shared = analyse(corpus);
|
|
42
|
+
const targets = options.only ?? corpus.briefs;
|
|
43
|
+
const findings = [];
|
|
44
|
+
for (const brief of targets) {
|
|
45
|
+
for (const { id, rule, severity, options: ruleOptions } of active) {
|
|
46
|
+
const results = await rule.check({
|
|
47
|
+
brief,
|
|
48
|
+
corpus,
|
|
49
|
+
config: corpus.config,
|
|
50
|
+
repoFiles: options.repoFiles ?? null,
|
|
51
|
+
shared,
|
|
52
|
+
options: ruleOptions,
|
|
53
|
+
});
|
|
54
|
+
for (const result of results) {
|
|
55
|
+
const configured = severity;
|
|
56
|
+
findings.push({
|
|
57
|
+
rule: id,
|
|
58
|
+
severity: result.severity === undefined ? configured : lower(result.severity, configured),
|
|
59
|
+
message: result.message,
|
|
60
|
+
file: brief.file,
|
|
61
|
+
line: Math.max(1, Math.trunc(result.line)),
|
|
62
|
+
brief: brief.id ?? undefined,
|
|
63
|
+
hint: result.hint,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return sortFindings(findings);
|
|
69
|
+
}
|
|
70
|
+
/** Strings by code unit, which is the same order on every host and in every locale. */
|
|
71
|
+
function order(a, b) {
|
|
72
|
+
if (a === b)
|
|
73
|
+
return 0;
|
|
74
|
+
return a < b ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
export function sortFindings(findings) {
|
|
77
|
+
return [...findings].sort((a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message));
|
|
78
|
+
}
|
|
79
|
+
export function summarise(findings) {
|
|
80
|
+
return {
|
|
81
|
+
errors: findings.filter((f) => f.severity === 'error').length,
|
|
82
|
+
warnings: findings.filter((f) => f.severity === 'warning').length,
|
|
83
|
+
notes: findings.filter((f) => f.severity === 'note').length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Whether findings fail a run: any error, or any warning under `--strict`. */
|
|
87
|
+
export function failing(findings, strict) {
|
|
88
|
+
return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Running rules: severities from the configuration, plugins beside the\n * built-in rules, findings in a stable order.\n */\n\nimport type { Brief } from './brief.js';\nimport { ConfigError } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { analyse, COLLISION_RULES, type Rule, RULES } from './rules.js';\nimport type { Finding, Severity, SeveritySetting } from './types.js';\n\nexport interface Plugin {\n /** Prefixes the plugin's rule ids: `<name>/<rule>`. */\n readonly name: string;\n readonly rules: readonly Rule[];\n /** The plugin's entry in the configuration, handed to each rule. */\n readonly options?: unknown;\n}\n\nexport interface LintOptions {\n readonly plugins?: readonly Plugin[];\n /** Tracked files, for the rules that read the tree. */\n readonly repoFiles?: readonly string[] | null;\n /** Limit the findings to these briefs. The whole corpus is still read. */\n readonly only?: readonly Brief[];\n}\n\nconst RANK: Readonly<Record<Severity, number>> = { note: 0, warning: 1, error: 2 };\n\nfunction lower(a: Severity, b: Severity): Severity {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\ninterface ActiveRule {\n readonly id: string;\n readonly rule: Rule;\n readonly severity: SeveritySetting;\n readonly options: unknown;\n}\n\n/** Every rule id a run knows: built-in, collision and plugin. */\nexport function ruleIds(plugins: readonly Plugin[] = []): string[] {\n return [\n ...RULES.map((r) => r.id),\n ...COLLISION_RULES.map((r) => r.id),\n ...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),\n ];\n}\n\n/** The severity configuration assigns a rule, or its own. */\nexport function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting {\n return corpus.config.rules[id] ?? fallback;\n}\n\n/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */\nexport function checkRuleIds(corpus: Corpus, plugins: readonly Plugin[] = []): void {\n const known = new Set(ruleIds(plugins));\n const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));\n if (unknown.length > 0) {\n throw new ConfigError('configuration', unknown.map((id) => `\"rules.${id}\" names no rule`));\n }\n}\n\nexport async function lint(corpus: Corpus, options: LintOptions = {}): Promise<Finding[]> {\n const plugins = options.plugins ?? [];\n checkRuleIds(corpus, plugins);\n const active: ActiveRule[] = [\n ...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),\n ...plugins.flatMap((plugin) =>\n plugin.rules.map((rule) => {\n const id = `${plugin.name}/${rule.id}`;\n return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };\n }),\n ),\n ].filter((r) => r.severity !== 'off');\n\n const shared = analyse(corpus);\n const targets = options.only ?? corpus.briefs;\n const findings: Finding[] = [];\n for (const brief of targets) {\n for (const { id, rule, severity, options: ruleOptions } of active) {\n const results = await rule.check({\n brief,\n corpus,\n config: corpus.config,\n repoFiles: options.repoFiles ?? null,\n shared,\n options: ruleOptions,\n });\n for (const result of results) {\n const configured = severity as Severity;\n findings.push({\n rule: id,\n severity: result.severity === undefined ? configured : lower(result.severity, configured),\n message: result.message,\n file: brief.file,\n line: Math.max(1, Math.trunc(result.line)),\n brief: brief.id ?? undefined,\n hint: result.hint,\n });\n }\n }\n }\n return sortFindings(findings);\n}\n\n/** Strings by code unit, which is the same order on every host and in every locale. */\nfunction order(a: string, b: string): number {\n if (a === b) return 0;\n return a < b ? -1 : 1;\n}\n\nexport function sortFindings(findings: readonly Finding[]): Finding[] {\n return [...findings].sort(\n (a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message),\n );\n}\n\nexport interface Summary {\n readonly errors: number;\n readonly warnings: number;\n readonly notes: number;\n}\n\nexport function summarise(findings: readonly Finding[]): Summary {\n return {\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n notes: findings.filter((f) => f.severity === 'note').length,\n };\n}\n\n/** Whether findings fail a run: any error, or any warning under `--strict`. */\nexport function failing(findings: readonly Finding[], strict: boolean): boolean {\n return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));\n}\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A structural Markdown scanner: code, comments, headings, task items and link
|
|
3
|
+
* destinations, each with its line.
|
|
4
|
+
*
|
|
5
|
+
* Not a CommonMark parser, and it does not need to be. Nothing here renders.
|
|
6
|
+
* What must be exactly right is knowing what is code or a comment, because a
|
|
7
|
+
* heading inside a fenced block is not a section and a template hint inside
|
|
8
|
+
* `<!-- -->` is not content. Everything else is deliberately simple: ATX
|
|
9
|
+
* headings only (a setext underline and a front-matter delimiter are the same
|
|
10
|
+
* three characters), fences at any indentation, and no indented code blocks,
|
|
11
|
+
* because inside a list four spaces of indentation is a continuation far more
|
|
12
|
+
* often than code.
|
|
13
|
+
*/
|
|
14
|
+
export interface Heading {
|
|
15
|
+
/** 0-based line. */
|
|
16
|
+
readonly line: number;
|
|
17
|
+
readonly level: number;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Section {
|
|
21
|
+
readonly heading: Heading;
|
|
22
|
+
/** 0-based line after the section's last line. The body starts on the line after the heading. */
|
|
23
|
+
readonly end: number;
|
|
24
|
+
}
|
|
25
|
+
export interface TaskItem {
|
|
26
|
+
/** 0-based line of the box. */
|
|
27
|
+
readonly line: number;
|
|
28
|
+
/** 0-based line after the item's last continuation line. */
|
|
29
|
+
readonly end: number;
|
|
30
|
+
readonly checked: boolean;
|
|
31
|
+
readonly text: string;
|
|
32
|
+
}
|
|
33
|
+
export interface LinkDestination {
|
|
34
|
+
/** 0-based line. */
|
|
35
|
+
readonly line: number;
|
|
36
|
+
/** Columns of the destination within the line, end exclusive. */
|
|
37
|
+
readonly start: number;
|
|
38
|
+
readonly end: number;
|
|
39
|
+
readonly target: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Scan {
|
|
42
|
+
readonly lines: readonly string[];
|
|
43
|
+
/** Lines with comments blanked and code kept: what counts as content. */
|
|
44
|
+
readonly prose: readonly string[];
|
|
45
|
+
/** Lines with comments, code spans and fenced blocks blanked: what counts as structure. */
|
|
46
|
+
readonly masked: readonly string[];
|
|
47
|
+
readonly headings: readonly Heading[];
|
|
48
|
+
readonly tasks: readonly TaskItem[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Scans `lines` from line `from`; earlier lines (front matter) are blanked in
|
|
52
|
+
* both masks and contribute nothing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function scan(lines: readonly string[], from?: number): Scan;
|
|
55
|
+
/** Sections: every heading below the title, running to the next heading at its level or above. */
|
|
56
|
+
export declare function sectionsOf(result: Scan): Section[];
|
|
57
|
+
/** The first level-one heading, which is what a reader takes as the title. */
|
|
58
|
+
export declare function titleOf(result: Scan): Heading | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Link and image destinations outside code and comments: `[text](dest)`,
|
|
61
|
+
* ``, and reference definitions `[label]: dest`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function linksOf(result: Scan): LinkDestination[];
|
|
64
|
+
/** Whether a line carries anything once comments are removed. */
|
|
65
|
+
export declare function hasContent(proseLine: string): boolean;
|