@trackunit/repo-checks 0.0.0-stage → 0.0.2
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 +3 -0
- package/LICENSE.txt +191 -0
- package/README.md +319 -2
- package/bin/index.d.ts +2 -0
- package/bin/index.js +34 -0
- package/migrations/entry.js +3 -0
- package/migrations.json +3 -0
- package/package.json +34 -3
- package/src/cli.d.ts +27 -0
- package/src/cli.js +109 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +21 -0
- package/src/lib/config-error.d.ts +8 -0
- package/src/lib/config-error.js +16 -0
- package/src/lib/config.d.ts +22 -0
- package/src/lib/config.js +61 -0
- package/src/lib/doc-references/case-fix.d.ts +21 -0
- package/src/lib/doc-references/case-fix.js +66 -0
- package/src/lib/doc-references/check-doc-references.d.ts +28 -0
- package/src/lib/doc-references/check-doc-references.js +147 -0
- package/src/lib/doc-references/classify.d.ts +49 -0
- package/src/lib/doc-references/classify.js +59 -0
- package/src/lib/doc-references/discover.d.ts +6 -0
- package/src/lib/doc-references/discover.js +84 -0
- package/src/lib/doc-references/extract.d.ts +29 -0
- package/src/lib/doc-references/extract.js +46 -0
- package/src/lib/doc-references/finding.d.ts +24 -0
- package/src/lib/doc-references/finding.js +11 -0
- package/src/lib/doc-references/fs-errors.d.ts +14 -0
- package/src/lib/doc-references/fs-errors.js +25 -0
- package/src/lib/doc-references/headings.d.ts +79 -0
- package/src/lib/doc-references/headings.js +219 -0
- package/src/lib/doc-references/parse.d.ts +72 -0
- package/src/lib/doc-references/parse.js +72 -0
- package/src/lib/doc-references/placeholders.d.ts +74 -0
- package/src/lib/doc-references/placeholders.js +176 -0
- package/src/lib/doc-references/repo-root.d.ts +18 -0
- package/src/lib/doc-references/repo-root.js +36 -0
- package/src/lib/doc-references/resolver.d.ts +96 -0
- package/src/lib/doc-references/resolver.js +430 -0
- package/src/lib/doc-references/settings.d.ts +163 -0
- package/src/lib/doc-references/settings.js +167 -0
- package/src/lib/doc-references/suppressions.d.ts +8 -0
- package/src/lib/doc-references/suppressions.js +24 -0
- package/src/lib/format.d.ts +13 -0
- package/src/lib/format.js +33 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** A 1-based source location, as mdast reports it. */
|
|
2
|
+
export interface MarkdownPoint {
|
|
3
|
+
readonly line: number;
|
|
4
|
+
readonly column: number;
|
|
5
|
+
readonly offset?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The structural subset of an mdast node the doc-reference checks read. Declared here rather than
|
|
9
|
+
* imported from `mdast` so the library doesn't depend on the parser's type packages.
|
|
10
|
+
*/
|
|
11
|
+
export interface MarkdownNode {
|
|
12
|
+
readonly type: string;
|
|
13
|
+
/** Text content: `code`, `inlineCode`, `yaml`, `html`, `text`, and similar leaves. */
|
|
14
|
+
readonly value?: string;
|
|
15
|
+
/** Destination of `link`, `image`, and `definition`. */
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
readonly children?: ReadonlyArray<MarkdownNode>;
|
|
18
|
+
readonly position?: {
|
|
19
|
+
readonly start: MarkdownPoint;
|
|
20
|
+
readonly end: MarkdownPoint;
|
|
21
|
+
};
|
|
22
|
+
/** The label as written, on `footnoteDefinition` and `footnoteReference` (and other reference nodes). */
|
|
23
|
+
readonly label?: string | null;
|
|
24
|
+
/** The normalised label, on the same nodes as `label`. */
|
|
25
|
+
readonly identifier?: string;
|
|
26
|
+
/** An MDX JSX element's name (`div`, `Meta`, `Docs.Block`), or `null` for a fragment. */
|
|
27
|
+
readonly name?: string | null;
|
|
28
|
+
/** On MDX JSX elements only. */
|
|
29
|
+
readonly attributes?: ReadonlyArray<{
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly name?: string;
|
|
32
|
+
readonly value?: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
/** On MDX ESM and `{expression}` nodes: the program the parser built from `value`. */
|
|
35
|
+
readonly data?: {
|
|
36
|
+
readonly estree?: {
|
|
37
|
+
readonly body: ReadonlyArray<unknown>;
|
|
38
|
+
} | null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether `node` is an MDX `{expression}` holding no code, only comments if anything (`{/* note *\/}`,
|
|
43
|
+
* `{// note}`), so it renders nothing; its source text, comments included, is its `value`. Decided
|
|
44
|
+
* from the program the parser built, so `{/* a *\/ x /* b *\/}` is code; an expression without one is.
|
|
45
|
+
*/
|
|
46
|
+
export declare const isCommentExpression: (node: MarkdownNode) => boolean;
|
|
47
|
+
/** A document that could not be parsed, located at the 1-based line and column the parser stopped at. */
|
|
48
|
+
export declare class DocumentParseError extends Error {
|
|
49
|
+
readonly file: string;
|
|
50
|
+
readonly line: number;
|
|
51
|
+
readonly column: number;
|
|
52
|
+
readonly reason: string;
|
|
53
|
+
/**
|
|
54
|
+
* @param file The document, as it was passed to the parser.
|
|
55
|
+
* @param line 1-based line the parser stopped at.
|
|
56
|
+
* @param column 1-based column the parser stopped at.
|
|
57
|
+
* @param reason The parser's message.
|
|
58
|
+
*/
|
|
59
|
+
constructor(file: string, line: number, column: number, reason: string);
|
|
60
|
+
}
|
|
61
|
+
/** Whether `file` is parsed as MDX: its extension is `.mdx`, in any case. */
|
|
62
|
+
export declare const isMdxFile: (file: string) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Parse a markdown document into mdast with GFM and YAML frontmatter, the way every document this
|
|
65
|
+
* library checks is read: a leading `---` block becomes a `yaml` node rather than content, while `+++` fences stay plain
|
|
66
|
+
* markdown, as on GitHub. A `.mdx` `file` is parsed with the MDX extensions too, as Storybook's MDX 3
|
|
67
|
+
* compiler reads it: ESM becomes `mdxjsEsm`, JSX `mdxJsxFlowElement`/`mdxJsxTextElement` (markdown in
|
|
68
|
+
* their children stays mdast; attributes are not children), and `{expressions}`, comments included,
|
|
69
|
+
* `mdxFlowExpression`/`mdxTextExpression` leaves. Plain markdown always parses; invalid MDX throws a
|
|
70
|
+
* {@link DocumentParseError} naming `file` and the location.
|
|
71
|
+
*/
|
|
72
|
+
export declare const parseMarkdown: (source: string, file: string) => MarkdownNode;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMarkdown = exports.isMdxFile = exports.DocumentParseError = exports.isCommentExpression = void 0;
|
|
4
|
+
const mdast_util_from_markdown_1 = require("mdast-util-from-markdown");
|
|
5
|
+
const mdast_util_frontmatter_1 = require("mdast-util-frontmatter");
|
|
6
|
+
const mdast_util_gfm_1 = require("mdast-util-gfm");
|
|
7
|
+
const mdast_util_mdx_1 = require("mdast-util-mdx");
|
|
8
|
+
const micromark_extension_frontmatter_1 = require("micromark-extension-frontmatter");
|
|
9
|
+
const micromark_extension_gfm_1 = require("micromark-extension-gfm");
|
|
10
|
+
const micromark_extension_mdxjs_1 = require("micromark-extension-mdxjs");
|
|
11
|
+
const MDX_EXPRESSION_TYPES = new Set(["mdxFlowExpression", "mdxTextExpression"]);
|
|
12
|
+
/**
|
|
13
|
+
* Whether `node` is an MDX `{expression}` holding no code, only comments if anything (`{/* note *\/}`,
|
|
14
|
+
* `{// note}`), so it renders nothing; its source text, comments included, is its `value`. Decided
|
|
15
|
+
* from the program the parser built, so `{/* a *\/ x /* b *\/}` is code; an expression without one is.
|
|
16
|
+
*/
|
|
17
|
+
const isCommentExpression = (node) => MDX_EXPRESSION_TYPES.has(node.type) && node.data?.estree?.body.length === 0;
|
|
18
|
+
exports.isCommentExpression = isCommentExpression;
|
|
19
|
+
/** A document that could not be parsed, located at the 1-based line and column the parser stopped at. */
|
|
20
|
+
class DocumentParseError extends Error {
|
|
21
|
+
/**
|
|
22
|
+
* @param file The document, as it was passed to the parser.
|
|
23
|
+
* @param line 1-based line the parser stopped at.
|
|
24
|
+
* @param column 1-based column the parser stopped at.
|
|
25
|
+
* @param reason The parser's message.
|
|
26
|
+
*/
|
|
27
|
+
constructor(file, line, column, reason) {
|
|
28
|
+
super(`${file}:${line}:${column}: ${reason}`);
|
|
29
|
+
this.file = file;
|
|
30
|
+
this.line = line;
|
|
31
|
+
this.column = column;
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
this.name = "DocumentParseError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.DocumentParseError = DocumentParseError;
|
|
37
|
+
const positiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
38
|
+
const toParseError = (error, file) => {
|
|
39
|
+
const message = typeof error === "object" && error !== null ? error : {};
|
|
40
|
+
const reason = typeof message.reason === "string"
|
|
41
|
+
? message.reason
|
|
42
|
+
: typeof message.message === "string"
|
|
43
|
+
? message.message
|
|
44
|
+
: String(error);
|
|
45
|
+
return new DocumentParseError(file, positiveInteger(message.line) ?? 1, positiveInteger(message.column) ?? 1, reason);
|
|
46
|
+
};
|
|
47
|
+
/** Whether `file` is parsed as MDX: its extension is `.mdx`, in any case. */
|
|
48
|
+
const isMdxFile = (file) => file.toLowerCase().endsWith(".mdx");
|
|
49
|
+
exports.isMdxFile = isMdxFile;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a markdown document into mdast with GFM and YAML frontmatter, the way every document this
|
|
52
|
+
* library checks is read: a leading `---` block becomes a `yaml` node rather than content, while `+++` fences stay plain
|
|
53
|
+
* markdown, as on GitHub. A `.mdx` `file` is parsed with the MDX extensions too, as Storybook's MDX 3
|
|
54
|
+
* compiler reads it: ESM becomes `mdxjsEsm`, JSX `mdxJsxFlowElement`/`mdxJsxTextElement` (markdown in
|
|
55
|
+
* their children stays mdast; attributes are not children), and `{expressions}`, comments included,
|
|
56
|
+
* `mdxFlowExpression`/`mdxTextExpression` leaves. Plain markdown always parses; invalid MDX throws a
|
|
57
|
+
* {@link DocumentParseError} naming `file` and the location.
|
|
58
|
+
*/
|
|
59
|
+
const parseMarkdown = (source, file) => {
|
|
60
|
+
const mdx = (0, exports.isMdxFile)(file);
|
|
61
|
+
try {
|
|
62
|
+
return (0, mdast_util_from_markdown_1.fromMarkdown)(source, {
|
|
63
|
+
extensions: [(0, micromark_extension_gfm_1.gfm)(), (0, micromark_extension_frontmatter_1.frontmatter)(["yaml"]), ...(mdx ? [(0, micromark_extension_mdxjs_1.mdxjs)()] : [])],
|
|
64
|
+
mdastExtensions: [(0, mdast_util_gfm_1.gfmFromMarkdown)(), (0, mdast_util_frontmatter_1.frontmatterFromMarkdown)(["yaml"]), ...(mdx ? [(0, mdast_util_mdx_1.mdxFromMarkdown)()] : [])],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw toParseError(error, file);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
exports.parseMarkdown = parseMarkdown;
|
|
72
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder conventions in documented paths, such as `docs/<skill>/SKILL.md`.
|
|
3
|
+
*
|
|
4
|
+
* A reference's path part is compiled segment by segment (split on `/`). A segment without a
|
|
5
|
+
* placeholder is `literal`; one with placeholders is a `pattern` of text and named slots. Each slot
|
|
6
|
+
* matches one or more characters of a single directory entry name, so it never matches across `/`
|
|
7
|
+
* and never matches the empty string. Slots with the same name, in any segment and from any
|
|
8
|
+
* convention, must bind to the same value. Text beside a slot (`eval-` in `eval-<skill>`) is matched
|
|
9
|
+
* case-exactly, so a case difference there reads as no match rather than a case mismatch.
|
|
10
|
+
*
|
|
11
|
+
* Named conventions:
|
|
12
|
+
* - `angle`: `<name>`.
|
|
13
|
+
* - `curly`: `{name}`, but not a brace pair that is part of `{{name}}`.
|
|
14
|
+
* - `double-curly`: `{{name}}`.
|
|
15
|
+
* - `colon`: `:name` (a letter or `_`, then word characters), only at the start of a segment, so
|
|
16
|
+
* `/:id/` and `:id.md` are placeholders but `app:build` is literal.
|
|
17
|
+
* - `path-to`: a leading `path/to/` (after any `./`) followed by at least one segment marks the
|
|
18
|
+
* whole reference as illustrative, so it is never checked (see `isIllustrativeReference`).
|
|
19
|
+
*
|
|
20
|
+
* Any other string is a custom regex, applied within a segment, whose single capture group is the
|
|
21
|
+
* placeholder name, e.g. `\[(\w+)\]` for `[slug]`. Where conventions overlap at a position, the
|
|
22
|
+
* longest match wins. `*` is not a placeholder: globs are skipped before resolution.
|
|
23
|
+
*/
|
|
24
|
+
export declare const NAMED_PLACEHOLDERS: readonly ["angle", "curly", "double-curly", "colon", "path-to"];
|
|
25
|
+
export type NamedPlaceholder = (typeof NAMED_PLACEHOLDERS)[number];
|
|
26
|
+
/** Every named convention; a single constant so measurement can narrow the defaults in one place. */
|
|
27
|
+
export declare const DEFAULT_PLACEHOLDERS: ReadonlyArray<string>;
|
|
28
|
+
export type PatternPart = {
|
|
29
|
+
readonly kind: "text";
|
|
30
|
+
readonly value: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "slot";
|
|
33
|
+
readonly name: string;
|
|
34
|
+
};
|
|
35
|
+
export type SegmentPattern = {
|
|
36
|
+
readonly kind: "literal";
|
|
37
|
+
readonly value: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "pattern";
|
|
40
|
+
readonly parts: ReadonlyArray<PatternPart>;
|
|
41
|
+
};
|
|
42
|
+
export interface CompiledReference {
|
|
43
|
+
readonly segments: ReadonlyArray<SegmentPattern>;
|
|
44
|
+
}
|
|
45
|
+
/** Placeholder name to the entry-name substring it matched. */
|
|
46
|
+
export type Bindings = ReadonlyMap<string, string>;
|
|
47
|
+
/** The number of capture groups in a regex source, or `undefined` when it doesn't compile. */
|
|
48
|
+
export declare const countCaptureGroups: (source: string) => number | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Whether `pathPart` starts with `path/to/` (after any `./`) and names something below it, with
|
|
51
|
+
* `path-to` enabled. Such a reference stands for "some file in your project", not a real path.
|
|
52
|
+
*/
|
|
53
|
+
export declare const isIllustrativeReference: (pathPart: string, conventions: ReadonlyArray<string>) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Compile a reference's path part (fragment stripped, URL-decoded, leading `/` removed) into
|
|
56
|
+
* per-segment matchers. Returns `undefined` when it has no placeholder under `conventions`, so the
|
|
57
|
+
* literal resolution applies unchanged. `.`, `..`, and empty segments stay literal; the resolver
|
|
58
|
+
* applies them lexically.
|
|
59
|
+
*/
|
|
60
|
+
export declare const compileReference: (pathPart: string, conventions: ReadonlyArray<string>) => CompiledReference | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Whether some segment has two placeholders with no text between them (`<a><b>.md`). Such a split
|
|
63
|
+
* is arbitrary and its regex backtracks polynomially on names that don't match, so it is skipped.
|
|
64
|
+
*/
|
|
65
|
+
export declare const hasAdjacentSlots: ({ segments }: CompiledReference) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A matcher for one directory entry name against `segment`, given the names bound by earlier
|
|
68
|
+
* segments. It returns the extended bindings, or `undefined` when the name doesn't match. Already
|
|
69
|
+
* bound names must reappear verbatim. When a segment has several unbound slots, only the regex
|
|
70
|
+
* engine's first split is tried (e.g. `<a>-<b>` on `x-y-z` binds `a=x-y`).
|
|
71
|
+
*/
|
|
72
|
+
export declare const createSegmentMatcher: (segment: Extract<SegmentPattern, {
|
|
73
|
+
kind: "pattern";
|
|
74
|
+
}>, bindings: Bindings) => ((name: string) => Bindings | undefined);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder conventions in documented paths, such as `docs/<skill>/SKILL.md`.
|
|
4
|
+
*
|
|
5
|
+
* A reference's path part is compiled segment by segment (split on `/`). A segment without a
|
|
6
|
+
* placeholder is `literal`; one with placeholders is a `pattern` of text and named slots. Each slot
|
|
7
|
+
* matches one or more characters of a single directory entry name, so it never matches across `/`
|
|
8
|
+
* and never matches the empty string. Slots with the same name, in any segment and from any
|
|
9
|
+
* convention, must bind to the same value. Text beside a slot (`eval-` in `eval-<skill>`) is matched
|
|
10
|
+
* case-exactly, so a case difference there reads as no match rather than a case mismatch.
|
|
11
|
+
*
|
|
12
|
+
* Named conventions:
|
|
13
|
+
* - `angle`: `<name>`.
|
|
14
|
+
* - `curly`: `{name}`, but not a brace pair that is part of `{{name}}`.
|
|
15
|
+
* - `double-curly`: `{{name}}`.
|
|
16
|
+
* - `colon`: `:name` (a letter or `_`, then word characters), only at the start of a segment, so
|
|
17
|
+
* `/:id/` and `:id.md` are placeholders but `app:build` is literal.
|
|
18
|
+
* - `path-to`: a leading `path/to/` (after any `./`) followed by at least one segment marks the
|
|
19
|
+
* whole reference as illustrative, so it is never checked (see `isIllustrativeReference`).
|
|
20
|
+
*
|
|
21
|
+
* Any other string is a custom regex, applied within a segment, whose single capture group is the
|
|
22
|
+
* placeholder name, e.g. `\[(\w+)\]` for `[slug]`. Where conventions overlap at a position, the
|
|
23
|
+
* longest match wins. `*` is not a placeholder: globs are skipped before resolution.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createSegmentMatcher = exports.hasAdjacentSlots = exports.compileReference = exports.isIllustrativeReference = exports.countCaptureGroups = exports.DEFAULT_PLACEHOLDERS = exports.NAMED_PLACEHOLDERS = void 0;
|
|
27
|
+
exports.NAMED_PLACEHOLDERS = ["angle", "curly", "double-curly", "colon", "path-to"];
|
|
28
|
+
/** Every named convention; a single constant so measurement can narrow the defaults in one place. */
|
|
29
|
+
exports.DEFAULT_PLACEHOLDERS = exports.NAMED_PLACEHOLDERS;
|
|
30
|
+
/** `path-to` marks a whole reference, so it has no in-segment token. */
|
|
31
|
+
const NAMED_TOKEN_SOURCES = {
|
|
32
|
+
angle: "<([^<>]+)>",
|
|
33
|
+
curly: "(?<!\\{)\\{([^{}]+)\\}(?!\\})",
|
|
34
|
+
"double-curly": "\\{\\{([^{}]+)\\}\\}",
|
|
35
|
+
colon: ":([A-Za-z_]\\w*)",
|
|
36
|
+
"path-to": undefined,
|
|
37
|
+
};
|
|
38
|
+
const isNamedPlaceholder = (convention) => exports.NAMED_PLACEHOLDERS.some(named => named === convention);
|
|
39
|
+
/** The number of capture groups in a regex source, or `undefined` when it doesn't compile. */
|
|
40
|
+
const countCaptureGroups = (source) => {
|
|
41
|
+
try {
|
|
42
|
+
return (new RegExp(`${source}|`).exec("")?.length ?? 1) - 1;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
exports.countCaptureGroups = countCaptureGroups;
|
|
49
|
+
const scannerFor = (convention) => {
|
|
50
|
+
const source = isNamedPlaceholder(convention) ? NAMED_TOKEN_SOURCES[convention] : convention;
|
|
51
|
+
return source === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: { regex: new RegExp(source, "y"), segmentStartOnly: convention === "colon" };
|
|
54
|
+
};
|
|
55
|
+
const compiledScanners = new WeakMap();
|
|
56
|
+
const scannersFor = (conventions) => {
|
|
57
|
+
let scanners = compiledScanners.get(conventions);
|
|
58
|
+
if (!scanners) {
|
|
59
|
+
scanners = conventions.flatMap(convention => scannerFor(convention) ?? []);
|
|
60
|
+
compiledScanners.set(conventions, scanners);
|
|
61
|
+
}
|
|
62
|
+
return scanners;
|
|
63
|
+
};
|
|
64
|
+
/** The longest placeholder token starting at `index`, if any. */
|
|
65
|
+
const tokenAt = (segment, index, scanners) => {
|
|
66
|
+
let best;
|
|
67
|
+
for (const { regex, segmentStartOnly } of scanners) {
|
|
68
|
+
if (segmentStartOnly && index !== 0)
|
|
69
|
+
continue;
|
|
70
|
+
regex.lastIndex = index;
|
|
71
|
+
const match = regex.exec(segment);
|
|
72
|
+
const name = match?.[1];
|
|
73
|
+
if (match && name && match[0].length > (best?.length ?? 0)) {
|
|
74
|
+
best = { length: match[0].length, name };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
};
|
|
79
|
+
const compileSegment = (segment, scanners) => {
|
|
80
|
+
const parts = [];
|
|
81
|
+
let text = "";
|
|
82
|
+
let index = 0;
|
|
83
|
+
while (index < segment.length) {
|
|
84
|
+
const token = tokenAt(segment, index, scanners);
|
|
85
|
+
if (token) {
|
|
86
|
+
if (text !== "")
|
|
87
|
+
parts.push({ kind: "text", value: text });
|
|
88
|
+
text = "";
|
|
89
|
+
parts.push({ kind: "slot", name: token.name });
|
|
90
|
+
index += token.length;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
text += segment[index];
|
|
94
|
+
index++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!parts.some(part => part.kind === "slot")) {
|
|
98
|
+
return { kind: "literal", value: segment };
|
|
99
|
+
}
|
|
100
|
+
if (text !== "")
|
|
101
|
+
parts.push({ kind: "text", value: text });
|
|
102
|
+
return { kind: "pattern", parts };
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Whether `pathPart` starts with `path/to/` (after any `./`) and names something below it, with
|
|
106
|
+
* `path-to` enabled. Such a reference stands for "some file in your project", not a real path.
|
|
107
|
+
*/
|
|
108
|
+
const isIllustrativeReference = (pathPart, conventions) => {
|
|
109
|
+
if (!conventions.includes("path-to"))
|
|
110
|
+
return false;
|
|
111
|
+
const segments = pathPart.split("/");
|
|
112
|
+
let start = 0;
|
|
113
|
+
while (segments[start] === ".")
|
|
114
|
+
start++;
|
|
115
|
+
return segments[start] === "path" && segments[start + 1] === "to" && segments.slice(start + 2).join("/") !== "";
|
|
116
|
+
};
|
|
117
|
+
exports.isIllustrativeReference = isIllustrativeReference;
|
|
118
|
+
/**
|
|
119
|
+
* Compile a reference's path part (fragment stripped, URL-decoded, leading `/` removed) into
|
|
120
|
+
* per-segment matchers. Returns `undefined` when it has no placeholder under `conventions`, so the
|
|
121
|
+
* literal resolution applies unchanged. `.`, `..`, and empty segments stay literal; the resolver
|
|
122
|
+
* applies them lexically.
|
|
123
|
+
*/
|
|
124
|
+
const compileReference = (pathPart, conventions) => {
|
|
125
|
+
const scanners = scannersFor(conventions);
|
|
126
|
+
const segments = pathPart.split("/").map(segment => compileSegment(segment, scanners));
|
|
127
|
+
return segments.some(segment => segment.kind !== "literal") ? { segments } : undefined;
|
|
128
|
+
};
|
|
129
|
+
exports.compileReference = compileReference;
|
|
130
|
+
/**
|
|
131
|
+
* Whether some segment has two placeholders with no text between them (`<a><b>.md`). Such a split
|
|
132
|
+
* is arbitrary and its regex backtracks polynomially on names that don't match, so it is skipped.
|
|
133
|
+
*/
|
|
134
|
+
const hasAdjacentSlots = ({ segments }) => segments.some(segment => segment.kind === "pattern" &&
|
|
135
|
+
segment.parts.some((part, index) => part.kind === "slot" && segment.parts[index + 1]?.kind === "slot"));
|
|
136
|
+
exports.hasAdjacentSlots = hasAdjacentSlots;
|
|
137
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
138
|
+
/**
|
|
139
|
+
* A matcher for one directory entry name against `segment`, given the names bound by earlier
|
|
140
|
+
* segments. It returns the extended bindings, or `undefined` when the name doesn't match. Already
|
|
141
|
+
* bound names must reappear verbatim. When a segment has several unbound slots, only the regex
|
|
142
|
+
* engine's first split is tried (e.g. `<a>-<b>` on `x-y-z` binds `a=x-y`).
|
|
143
|
+
*/
|
|
144
|
+
const createSegmentMatcher = (segment, bindings) => {
|
|
145
|
+
const groups = new Map();
|
|
146
|
+
const source = segment.parts
|
|
147
|
+
.map(part => {
|
|
148
|
+
if (part.kind === "text")
|
|
149
|
+
return escapeRegExp(part.value);
|
|
150
|
+
const bound = bindings.get(part.name);
|
|
151
|
+
if (bound !== undefined)
|
|
152
|
+
return escapeRegExp(bound);
|
|
153
|
+
const group = groups.get(part.name);
|
|
154
|
+
if (group !== undefined)
|
|
155
|
+
return `\\k<${group}>`;
|
|
156
|
+
const newGroup = `p${groups.size}`;
|
|
157
|
+
groups.set(part.name, newGroup);
|
|
158
|
+
return `(?<${newGroup}>[^/]+)`;
|
|
159
|
+
})
|
|
160
|
+
.join("");
|
|
161
|
+
const regex = new RegExp(`^${source}$`);
|
|
162
|
+
return name => {
|
|
163
|
+
const match = regex.exec(name);
|
|
164
|
+
if (!match)
|
|
165
|
+
return undefined;
|
|
166
|
+
const next = new Map(bindings);
|
|
167
|
+
for (const [placeholder, group] of groups) {
|
|
168
|
+
const value = match.groups?.[group];
|
|
169
|
+
if (value !== undefined)
|
|
170
|
+
next.set(placeholder, value);
|
|
171
|
+
}
|
|
172
|
+
return next;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
exports.createSegmentMatcher = createSegmentMatcher;
|
|
176
|
+
//# sourceMappingURL=placeholders.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RepoRootOptions {
|
|
2
|
+
/** Explicit repository root; a relative value resolves against `cwd`. */
|
|
3
|
+
readonly rootDir?: string;
|
|
4
|
+
/** Working directory a relative `rootDir` resolves against. Defaults to `process.cwd()`. */
|
|
5
|
+
readonly cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Find the repository root for a directory: `rootDir` when configured, otherwise `startDir` or its
|
|
9
|
+
* nearest ancestor containing a `.git` directory or a `.git` file (a git worktree or submodule).
|
|
10
|
+
* Returns `undefined` when no repository encloses `startDir`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const findRepoRootFromDir: (startDir: string, { rootDir, cwd }?: RepoRootOptions) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Find the repository root for a checked document, as {@link findRepoRootFromDir} does from the
|
|
15
|
+
* document's directory. Walks up from the document rather than from `process.cwd()`, so checking
|
|
16
|
+
* from a subdirectory or a worktree resolves the same root.
|
|
17
|
+
*/
|
|
18
|
+
export declare const findRepoRoot: (documentPath: string, options?: RepoRootOptions) => string | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findRepoRoot = exports.findRepoRootFromDir = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
/**
|
|
8
|
+
* Find the repository root for a directory: `rootDir` when configured, otherwise `startDir` or its
|
|
9
|
+
* nearest ancestor containing a `.git` directory or a `.git` file (a git worktree or submodule).
|
|
10
|
+
* Returns `undefined` when no repository encloses `startDir`.
|
|
11
|
+
*/
|
|
12
|
+
const findRepoRootFromDir = (startDir, { rootDir, cwd } = {}) => {
|
|
13
|
+
if (rootDir !== undefined) {
|
|
14
|
+
return path.resolve(cwd ?? process.cwd(), rootDir);
|
|
15
|
+
}
|
|
16
|
+
let directory = path.resolve(startDir);
|
|
17
|
+
for (;;) {
|
|
18
|
+
if (fs.existsSync(path.join(directory, ".git"))) {
|
|
19
|
+
return directory;
|
|
20
|
+
}
|
|
21
|
+
const parent = path.dirname(directory);
|
|
22
|
+
if (parent === directory) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
directory = parent;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.findRepoRootFromDir = findRepoRootFromDir;
|
|
29
|
+
/**
|
|
30
|
+
* Find the repository root for a checked document, as {@link findRepoRootFromDir} does from the
|
|
31
|
+
* document's directory. Walks up from the document rather than from `process.cwd()`, so checking
|
|
32
|
+
* from a subdirectory or a worktree resolves the same root.
|
|
33
|
+
*/
|
|
34
|
+
const findRepoRoot = (documentPath, options = {}) => (0, exports.findRepoRootFromDir)(path.dirname(path.resolve(documentPath)), options);
|
|
35
|
+
exports.findRepoRoot = findRepoRoot;
|
|
36
|
+
//# sourceMappingURL=repo-root.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Case-exact resolution of markdown file references.
|
|
3
|
+
*
|
|
4
|
+
* Existence is decided by listing directories and comparing names exactly, never with
|
|
5
|
+
* `fs.existsSync`, which is case-insensitive on macOS. A reference that only matches
|
|
6
|
+
* case-insensitively is a `caseMismatch` on every host, so macOS and Linux CI agree on case.
|
|
7
|
+
*
|
|
8
|
+
* Existence is checked against the working tree, not the git index. A gitignored or uncommitted
|
|
9
|
+
* file counts as present, so a checkout that has one (e.g. generated by a setup script) can pass
|
|
10
|
+
* where a fresh CI checkout reports it missing, and a link to a new file passes before it is staged.
|
|
11
|
+
*/
|
|
12
|
+
export type ReferenceKind = "url" | "inline";
|
|
13
|
+
export type Resolution = {
|
|
14
|
+
readonly kind: "exact";
|
|
15
|
+
readonly path: string;
|
|
16
|
+
/** Set when the reference has placeholders, so `path` is one arbitrary match of it. */
|
|
17
|
+
readonly placeholder?: true;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "caseMismatch";
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly actualPath: string;
|
|
22
|
+
/** The directory the reference's path was resolved against: the document's directory or the repo root. */
|
|
23
|
+
readonly base: string;
|
|
24
|
+
/** A recased segment had several siblings matching it case-insensitively, so `actualPath` is one guess of several. */
|
|
25
|
+
readonly ambiguous: boolean;
|
|
26
|
+
/** The reference has placeholders, so `actualPath` is one instance of it rather than its corrected form. */
|
|
27
|
+
readonly placeholder: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "ignored";
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "missing";
|
|
32
|
+
};
|
|
33
|
+
export interface ResolveInput {
|
|
34
|
+
/** The reference as written: a link/image/definition URL or an inline-code span. */
|
|
35
|
+
readonly reference: string;
|
|
36
|
+
/** URLs resolve against the document (or the repo root with a leading `/`); inline code also tries the repo root. */
|
|
37
|
+
readonly referenceKind: ReferenceKind;
|
|
38
|
+
readonly documentDir: string;
|
|
39
|
+
readonly repoRoot: string;
|
|
40
|
+
/** Picomatch globs matched against the repo-relative path. */
|
|
41
|
+
readonly ignore: ReadonlyArray<string>;
|
|
42
|
+
/** Placeholder conventions that turn the reference into a wildcard pattern (see `placeholders.ts`). */
|
|
43
|
+
readonly placeholders: ReadonlyArray<string>;
|
|
44
|
+
/** Directory listings one placeholder reference may make before it is skipped. */
|
|
45
|
+
readonly maxVisitedDirectories?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A listing (or parsed file) read within this long of its mtime is never reused: a change in the
|
|
49
|
+
* same timestamp tick leaves the mtime unchanged. Covers the coarsest common granularity (FAT, 2s).
|
|
50
|
+
*/
|
|
51
|
+
export declare const RACY_WINDOW_MS = 2000;
|
|
52
|
+
/**
|
|
53
|
+
* The on-disk casing of an absolute path, without following symlinks. Segments from the first one
|
|
54
|
+
* that doesn't exist, or whose parent can't be listed, onward are returned as given. Callers can be
|
|
55
|
+
* handed a differently cased path on case-insensitive filesystems; canonicalising the document's own
|
|
56
|
+
* path keeps its relative references from all reading as case mismatches.
|
|
57
|
+
*/
|
|
58
|
+
export declare const canonicalCase: (absolutePath: string) => string;
|
|
59
|
+
/** Whether a repo-relative path (either separator) matches any of the ignore globs. */
|
|
60
|
+
export declare const isIgnoredPath: (repoRelativePath: string, ignore: ReadonlyArray<string>) => boolean;
|
|
61
|
+
export interface SplitReference {
|
|
62
|
+
/** The reference as written up to its suffix, still percent-encoded for URLs. */
|
|
63
|
+
readonly pathPart: string;
|
|
64
|
+
/** Everything from the first `?` (URLs only) or `#` on, e.g. `?plain=1#L3`; `""` when there is none. */
|
|
65
|
+
readonly suffix: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Split a reference into its path part and suffix. A URL's suffix starts at its `?query` or
|
|
69
|
+
* `#fragment`, whichever comes first. Inline code only drops a `#fragment`, since a `?` in a code
|
|
70
|
+
* span may be a literal part of the path.
|
|
71
|
+
*/
|
|
72
|
+
export declare const splitReference: (reference: string, referenceKind: ReferenceKind) => SplitReference;
|
|
73
|
+
/** A reference's `#fragment` without the `#`, still percent-encoded for URLs; `""` when there is none. */
|
|
74
|
+
export declare const referenceFragment: (reference: string) => string;
|
|
75
|
+
/** Whether a reference target ends in a file extension. One ending in `/` never does. */
|
|
76
|
+
export declare const hasFileExtension: (target: string) => boolean;
|
|
77
|
+
/** The path a reference names: its path part (see `splitReference`), percent-decoded for URLs. */
|
|
78
|
+
export declare const referenceTarget: (reference: string, referenceKind: ReferenceKind) => string;
|
|
79
|
+
/** Hard cap on directory listings per placeholder reference; hitting it skips the reference. */
|
|
80
|
+
export declare const MAX_VISITED_DIRECTORIES = 10000;
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a reference to `exact`, `caseMismatch`, `ignored`, or `missing`. Candidates inside an
|
|
83
|
+
* ignored location are never looked up. Across candidates, an exact match wins, then an ignored
|
|
84
|
+
* candidate, then the first case mismatch. A reference with placeholders (under
|
|
85
|
+
* `input.placeholders`) matches wildcards against the tree; see `lookupWildcard`. An illustrative
|
|
86
|
+
* `path/to/` reference, or one with adjacent placeholders, is `ignored` without any lookup.
|
|
87
|
+
*
|
|
88
|
+
* The reference as written says what it names: a trailing `/` must resolve to a directory, a file
|
|
89
|
+
* extension to a file (so a directory named `foo.md` is `missing`), and anything else to either. An
|
|
90
|
+
* entry of the wrong kind is `missing`, even when its name differs only in case.
|
|
91
|
+
*
|
|
92
|
+
* Only the repository is looked at: a candidate outside `repoRoot` is `missing` whether or not it
|
|
93
|
+
* exists on this machine, and a leading `..` in a root-relative reference stays at the root, as on
|
|
94
|
+
* GitHub (`/../x` is `/x`).
|
|
95
|
+
*/
|
|
96
|
+
export declare const resolveReference: (input: ResolveInput) => Resolution;
|