@trackunit/repo-checks 0.0.0-stage → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.txt +191 -0
  3. package/README.md +319 -2
  4. package/bin/index.d.ts +2 -0
  5. package/bin/index.js +34 -0
  6. package/migrations/entry.js +3 -0
  7. package/migrations.json +3 -0
  8. package/package.json +34 -3
  9. package/src/cli.d.ts +27 -0
  10. package/src/cli.js +109 -0
  11. package/src/index.d.ts +8 -0
  12. package/src/index.js +21 -0
  13. package/src/lib/config-error.d.ts +8 -0
  14. package/src/lib/config-error.js +16 -0
  15. package/src/lib/config.d.ts +22 -0
  16. package/src/lib/config.js +61 -0
  17. package/src/lib/doc-references/case-fix.d.ts +21 -0
  18. package/src/lib/doc-references/case-fix.js +66 -0
  19. package/src/lib/doc-references/check-doc-references.d.ts +28 -0
  20. package/src/lib/doc-references/check-doc-references.js +147 -0
  21. package/src/lib/doc-references/classify.d.ts +49 -0
  22. package/src/lib/doc-references/classify.js +59 -0
  23. package/src/lib/doc-references/discover.d.ts +6 -0
  24. package/src/lib/doc-references/discover.js +84 -0
  25. package/src/lib/doc-references/extract.d.ts +29 -0
  26. package/src/lib/doc-references/extract.js +46 -0
  27. package/src/lib/doc-references/finding.d.ts +24 -0
  28. package/src/lib/doc-references/finding.js +11 -0
  29. package/src/lib/doc-references/fs-errors.d.ts +14 -0
  30. package/src/lib/doc-references/fs-errors.js +25 -0
  31. package/src/lib/doc-references/headings.d.ts +79 -0
  32. package/src/lib/doc-references/headings.js +219 -0
  33. package/src/lib/doc-references/parse.d.ts +72 -0
  34. package/src/lib/doc-references/parse.js +72 -0
  35. package/src/lib/doc-references/placeholders.d.ts +74 -0
  36. package/src/lib/doc-references/placeholders.js +176 -0
  37. package/src/lib/doc-references/repo-root.d.ts +18 -0
  38. package/src/lib/doc-references/repo-root.js +36 -0
  39. package/src/lib/doc-references/resolver.d.ts +96 -0
  40. package/src/lib/doc-references/resolver.js +430 -0
  41. package/src/lib/doc-references/settings.d.ts +163 -0
  42. package/src/lib/doc-references/settings.js +167 -0
  43. package/src/lib/doc-references/suppressions.d.ts +8 -0
  44. package/src/lib/doc-references/suppressions.js +24 -0
  45. package/src/lib/format.d.ts +13 -0
  46. package/src/lib/format.js +33 -0
@@ -0,0 +1,49 @@
1
+ import { type ReferenceKind } from "./resolver";
2
+ /**
3
+ * Decide whether a markdown reference looks like a repository path worth resolving.
4
+ *
5
+ * The non-configurable exclusions are categorically not repo-relative paths: URLs with a scheme,
6
+ * container/registry image references, home-directory paths, globs, shell variables, multi-word
7
+ * command spans, Storybook `?path=` links, and bare `#anchor`s. The configurable `ignore` list is applied by the resolver,
8
+ * because globs match the repo-relative path, which depends on where the reference resolves.
9
+ *
10
+ * Beyond those, the shape of the target decides. The target is the resolver's `referenceTarget`: a
11
+ * URL's `?query` and `#fragment` are stripped and it is percent-decoded (`guide%2Emd` has an
12
+ * extension), while inline code only loses a `#fragment`.
13
+ * - a `/` and a file extension (`docs/guide.md`): always checked;
14
+ * - a `/` and no extension (`libs/react/components/`, `docs/guides`): checked with `directories`;
15
+ * - no `/` and a file extension, in a link, image, or definition URL (`[x](README.md)`): always
16
+ * checked, since a URL with no `/` and no scheme is unambiguously a path relative to the
17
+ * document;
18
+ * - no `/` and a file extension, in inline code (`` `README.md` ``, `` `package.json` ``):
19
+ * checked with `bareFilenames`, since most inline-code spans with no `/` are package names,
20
+ * versions, or commands rather than paths (see `bareFilenames`'s doc comment in `settings.ts`);
21
+ * - no `/` and no extension, either way (`` `README` ``, `[x](README)`): checked only when both
22
+ * `bareFilenames` and `directories` are on.
23
+ * An extension is a final `.` and 1-8 letters or digits, so a bare `v1.2` or `console.log` counts
24
+ * as a filename too.
25
+ * The resolver then expects a file for an extension, a directory for a trailing `/`, and either
26
+ * otherwise (including a stemless dot-name such as `.github`, which is still checked here).
27
+ *
28
+ * Placeholder references (`docs/<name>.md`, `path/to/file.ts`) are passed on: the resolver matches
29
+ * them as wildcards, or skips illustrative `path/to/` ones, under the configured conventions. The file-path test is applied to the target, not
30
+ * to what its placeholders expand to, so a placeholder counts as having an extension only when a literal `.ext` ends it
31
+ * (`<name>.md` does, `<file>` and `file.<ext>` don't).
32
+ */
33
+ export type SkipReason = "empty" | "anchorOnly" | "multiWord" | "registryImage" | "urlScheme" | "homePath" | "shellVariable" | "glob" | "storybookPath" | "notAFilePath";
34
+ export type Classification = {
35
+ readonly check: true;
36
+ } | {
37
+ readonly check: false;
38
+ readonly reason: SkipReason;
39
+ };
40
+ /** The doc-references settings that widen which reference shapes are checked. */
41
+ export interface ClassifyOptions {
42
+ readonly directories: boolean;
43
+ readonly bareFilenames: boolean;
44
+ }
45
+ /**
46
+ * Whether a reference is checked as a repo path, or the reason it is skipped: empty, anchor-only,
47
+ * URL-, shell- or glob-shaped, a registry image, a Storybook path, or not shaped like a file path.
48
+ */
49
+ export declare const classifyReference: (reference: string | undefined, referenceKind: ReferenceKind, options?: ClassifyOptions) => Classification;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyReference = void 0;
4
+ const resolver_1 = require("./resolver");
5
+ const settings_1 = require("./settings");
6
+ /**
7
+ * A registry host (has a dot, or is `localhost`, optionally with a port), a repository path, and a
8
+ * `:tag` or `@sha256:` digest, e.g. `ghcr.io/org/img:1.2` or `123.dkr.ecr.eu-west-1.amazonaws.com/app:latest`.
9
+ * Checked before the URL-scheme test because `localhost:5000/...` would otherwise read as a scheme.
10
+ */
11
+ const REGISTRY_IMAGE_RE = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)+|localhost)(?::\d+)?\/[^\s:@]+(?::\w[\w.-]*|@sha256:[a-f0-9]+)$/i;
12
+ const URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
13
+ const STORYBOOK_PATH_QUERY_RE = /[?&]path=/u;
14
+ const DEFAULT_OPTIONS = { directories: settings_1.DEFAULT_DIRECTORIES, bareFilenames: settings_1.DEFAULT_BARE_FILENAMES };
15
+ const skip = (reason) => ({ check: false, reason });
16
+ const isCheckedShape = (base, referenceKind, { directories, bareFilenames }) => {
17
+ const hasExtension = (0, resolver_1.hasFileExtension)(base);
18
+ if (base.includes("/"))
19
+ return hasExtension || directories;
20
+ // A bare *named* file (has an extension, e.g. `README.md`) in a URL (link/image/definition) is
21
+ // unambiguously a relative path, so it is always checked. The same shape in inline code is often
22
+ // a package name or a version (`eslint@9`, `v1.2`), so it stays gated by `bareFilenames` there.
23
+ // An extensionless bare word (`README`) is ambiguous either way and keeps needing both
24
+ // `bareFilenames` and `directories`.
25
+ if (referenceKind === "url" && hasExtension)
26
+ return true;
27
+ return bareFilenames && (hasExtension || directories);
28
+ };
29
+ /**
30
+ * Whether a reference is checked as a repo path, or the reason it is skipped: empty, anchor-only,
31
+ * URL-, shell- or glob-shaped, a registry image, a Storybook path, or not shaped like a file path.
32
+ */
33
+ const classifyReference = (reference, referenceKind, options = DEFAULT_OPTIONS) => {
34
+ if (!reference)
35
+ return skip("empty");
36
+ if (reference.startsWith("#"))
37
+ return skip("anchorOnly");
38
+ if (/\s/.test(reference))
39
+ return skip("multiWord");
40
+ if (REGISTRY_IMAGE_RE.test(reference))
41
+ return skip("registryImage");
42
+ // `//host/path` is a protocol-relative URL, not a repo-root path.
43
+ if (URL_SCHEME_RE.test(reference) || reference.startsWith("//"))
44
+ return skip("urlScheme");
45
+ if (reference.startsWith("~"))
46
+ return skip("homePath");
47
+ if (reference.includes("$"))
48
+ return skip("shellVariable");
49
+ if (reference.includes("*"))
50
+ return skip("glob");
51
+ // A Storybook link (`?path=/docs/...`) addresses a page of the running Storybook, whatever path precedes it.
52
+ if (STORYBOOK_PATH_QUERY_RE.test(reference))
53
+ return skip("storybookPath");
54
+ if (!isCheckedShape((0, resolver_1.referenceTarget)(reference, referenceKind), referenceKind, options))
55
+ return skip("notAFilePath");
56
+ return { check: true };
57
+ };
58
+ exports.classifyReference = classifyReference;
59
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Every `.md` and `.mdx` document (extension in any case) under `rootDir`, as sorted absolute paths. Inside a git work tree
3
+ * these are git's tracked and untracked, non-ignored files that exist on disk; outside one, a
4
+ * filesystem walk that skips `.git` and conventional generated output.
5
+ */
6
+ export declare const discoverDocuments: (rootDir: string) => Array<string>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.discoverDocuments = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const fs_errors_1 = require("./fs-errors");
9
+ /** Document extensions discovered and checked, in any case (`.MD`), as `parseMarkdown` and the anchor check read them. */
10
+ const DOCUMENT_PATHSPECS = [":(icase)*.md", ":(icase)*.mdx"];
11
+ /** Directories the filesystem walk never enters: VCS metadata and conventional generated output. */
12
+ const WALK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage"]);
13
+ /** Set by git hooks; inherited, they would point `git ls-files` at the hook's repository instead of `rootDir`'s. */
14
+ const GIT_LOCATION_VARIABLES = new Set(["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]);
15
+ const isDocument = (name) => /\.mdx?$/iu.test(name);
16
+ /**
17
+ * Tracked and untracked, non-ignored documents under `rootDir`, as absolute paths, or `undefined`
18
+ * when `rootDir` is not inside a git work tree (or git is unavailable).
19
+ */
20
+ const listWithGit = (rootDir) => {
21
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !GIT_LOCATION_VARIABLES.has(name)));
22
+ const result = (0, child_process_1.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...DOCUMENT_PATHSPECS], { cwd: rootDir, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, env: { ...env, LC_ALL: "C", LANGUAGE: "C" } });
23
+ if (result.error) {
24
+ if ("code" in result.error && result.error.code === "ENOENT")
25
+ return undefined;
26
+ throw new fs_errors_1.ReadError(`Failed to list documents with git in ${rootDir}: ${result.error.message}`);
27
+ }
28
+ if (result.status !== 0) {
29
+ if (/not a git repository/i.test(result.stderr))
30
+ return undefined;
31
+ throw new fs_errors_1.ReadError(`git ls-files failed in ${rootDir}: ${result.stderr.trim()}`);
32
+ }
33
+ const files = new Set(result.stdout.split("\0").filter(file => file !== ""));
34
+ return [...files].map(file => path.join(rootDir, file));
35
+ };
36
+ const readDirectory = (directory) => {
37
+ try {
38
+ return fs.readdirSync(directory, { withFileTypes: true });
39
+ }
40
+ catch (error) {
41
+ throw new fs_errors_1.ReadError(`Cannot read directory ${directory}: ${(0, fs_errors_1.errorMessage)(error)}`);
42
+ }
43
+ };
44
+ const walk = (directory, documents) => {
45
+ for (const entry of readDirectory(directory)) {
46
+ const entryPath = path.join(directory, entry.name);
47
+ if (entry.isDirectory()) {
48
+ if (!WALK_SKIPPED_DIRECTORIES.has(entry.name))
49
+ walk(entryPath, documents);
50
+ }
51
+ else if (isDocument(entry.name)) {
52
+ documents.push(entryPath);
53
+ }
54
+ }
55
+ };
56
+ /**
57
+ * Whether `file` is a regular file on disk; `git ls-files --cached` still lists tracked files deleted from the work tree.
58
+ *
59
+ * @throws {ReadError} when `file` can't be stat'ed for any reason other than not existing
60
+ */
61
+ const existsAsFile = (file) => {
62
+ try {
63
+ return fs.statSync(file).isFile();
64
+ }
65
+ catch (error) {
66
+ if ((0, fs_errors_1.isNotFoundError)(error))
67
+ return false;
68
+ throw new fs_errors_1.ReadError(`Cannot read ${file}: ${(0, fs_errors_1.errorMessage)(error)}`);
69
+ }
70
+ };
71
+ /**
72
+ * Every `.md` and `.mdx` document (extension in any case) under `rootDir`, as sorted absolute paths. Inside a git work tree
73
+ * these are git's tracked and untracked, non-ignored files that exist on disk; outside one, a
74
+ * filesystem walk that skips `.git` and conventional generated output.
75
+ */
76
+ const discoverDocuments = (rootDir) => {
77
+ const listed = listWithGit(rootDir);
78
+ const documents = listed ?? [];
79
+ if (listed === undefined)
80
+ walk(rootDir, documents);
81
+ return documents.filter(existsAsFile).sort();
82
+ };
83
+ exports.discoverDocuments = discoverDocuments;
84
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1,29 @@
1
+ import type { MarkdownNode } from "./parse";
2
+ import type { ReferenceKind } from "./resolver";
3
+ import type { DocsSettings } from "./settings";
4
+ /** The markdown construct a reference was written in; the keys of {@link ConstructToggles}, singular. */
5
+ export type ReferenceConstruct = "link" | "image" | "definition" | "inlineCode";
6
+ /** A reference as written in a document, before it is classified or resolved. */
7
+ export interface ExtractedReference {
8
+ readonly kind: ReferenceConstruct;
9
+ /** The link, image, or definition URL as the parser decoded it, or the inline-code span's text. */
10
+ readonly target: string;
11
+ /** 1-based line of the construct's start. */
12
+ readonly line: number;
13
+ /** 1-based column of the construct's start. */
14
+ readonly column: number;
15
+ }
16
+ /** The resolver's reading of a construct: inline code is resolved as a bare path, everything else as a URL. */
17
+ export declare const referenceKindOf: (construct: ReferenceConstruct) => ReferenceKind;
18
+ /**
19
+ * Every reference in `tree` whose construct `settings.constructs` enables, in document order.
20
+ * Code blocks (fenced or indented) and frontmatter are leaves, so nothing inside them is extracted;
21
+ * so are MDX ESM and `{expressions}`, and a JSX element's attributes aren't children, so only the
22
+ * markdown in its children is.
23
+ * A link reference (`[x][r]`) is reported through its definition only, so each target is reported
24
+ * once. Autolinks and GFM literal URLs are links like any other; deciding which targets to check is
25
+ * the caller's job (see `classify.ts`).
26
+ * References starting on the line after an ignore-next-line comment are dropped (see
27
+ * `suppressions.ts`); a link reference's is decided on its definition's line.
28
+ */
29
+ export declare const extractReferences: (tree: MarkdownNode, settings: Pick<DocsSettings, "constructs">) => Array<ExtractedReference>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractReferences = exports.referenceKindOf = void 0;
4
+ const suppressions_1 = require("./suppressions");
5
+ const CONSTRUCT_TOGGLE = {
6
+ link: "links",
7
+ image: "images",
8
+ definition: "definitions",
9
+ inlineCode: "inlineCode",
10
+ };
11
+ const isReferenceConstruct = (type) => Object.hasOwn(CONSTRUCT_TOGGLE, type);
12
+ /** The resolver's reading of a construct: inline code is resolved as a bare path, everything else as a URL. */
13
+ const referenceKindOf = (construct) => construct === "inlineCode" ? "inline" : "url";
14
+ exports.referenceKindOf = referenceKindOf;
15
+ /**
16
+ * Every reference in `tree` whose construct `settings.constructs` enables, in document order.
17
+ * Code blocks (fenced or indented) and frontmatter are leaves, so nothing inside them is extracted;
18
+ * so are MDX ESM and `{expressions}`, and a JSX element's attributes aren't children, so only the
19
+ * markdown in its children is.
20
+ * A link reference (`[x][r]`) is reported through its definition only, so each target is reported
21
+ * once. Autolinks and GFM literal URLs are links like any other; deciding which targets to check is
22
+ * the caller's job (see `classify.ts`).
23
+ * References starting on the line after an ignore-next-line comment are dropped (see
24
+ * `suppressions.ts`); a link reference's is decided on its definition's line.
25
+ */
26
+ const extractReferences = (tree, settings) => {
27
+ const references = [];
28
+ const ignoredLines = new Set();
29
+ const visit = (node) => {
30
+ const ignoredLine = (0, suppressions_1.ignoredNextLine)(node);
31
+ if (ignoredLine !== undefined)
32
+ ignoredLines.add(ignoredLine);
33
+ if (isReferenceConstruct(node.type) && settings.constructs[CONSTRUCT_TOGGLE[node.type]]) {
34
+ const target = node.type === "inlineCode" ? node.value : node.url;
35
+ if (target !== undefined) {
36
+ const start = node.position?.start;
37
+ references.push({ kind: node.type, target, line: start?.line ?? 1, column: start?.column ?? 1 });
38
+ }
39
+ }
40
+ node.children?.forEach(visit);
41
+ };
42
+ visit(tree);
43
+ return references.filter(reference => !ignoredLines.has(reference.line));
44
+ };
45
+ exports.extractReferences = extractReferences;
46
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1,24 @@
1
+ import type { ReferenceConstruct } from "./extract";
2
+ /**
3
+ * Why a reference was reported: its target doesn't exist on disk (`missing`), exists only under
4
+ * different letter case (`caseMismatch`), or exists but its `#fragment` names no heading or anchor
5
+ * in it (`missingAnchor`).
6
+ */
7
+ export type FindingReason = "missing" | "caseMismatch" | "missingAnchor";
8
+ /** One reported doc reference. */
9
+ export interface Finding {
10
+ /** The document, as a POSIX path relative to the repository root. */
11
+ readonly file: string;
12
+ /** 1-based line of the reference's construct. */
13
+ readonly line: number;
14
+ /** 1-based column of the reference's construct. */
15
+ readonly column: number;
16
+ /** The reference as the parser read it: a link, image, or definition URL, or an inline-code span. */
17
+ readonly target: string;
18
+ readonly kind: ReferenceConstruct;
19
+ readonly reason: FindingReason;
20
+ /** A replacement for `target` that would resolve, when exactly one is known: the on-disk casing of a `caseMismatch`. */
21
+ readonly suggestion?: string;
22
+ }
23
+ /** Order findings by file, then line, then column, so output is the same on every host. */
24
+ export declare const compareFindings: (a: Finding, b: Finding) => number;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareFindings = void 0;
4
+ /** Order findings by file, then line, then column, so output is the same on every host. */
5
+ const compareFindings = (a, b) => {
6
+ if (a.file !== b.file)
7
+ return a.file < b.file ? -1 : 1;
8
+ return a.line - b.line || a.column - b.column;
9
+ };
10
+ exports.compareFindings = compareFindings;
11
+ //# sourceMappingURL=finding.js.map
@@ -0,0 +1,14 @@
1
+ /** Whether a filesystem error means the path doesn't exist, as opposed to existing but being unreadable. */
2
+ export declare const isNotFoundError: (error: unknown) => boolean;
3
+ /** Whether `error` is a Node system error (`EACCES`, `ELOOP`, ...) from a failed system call. */
4
+ export declare const isSystemError: (error: unknown) => boolean;
5
+ /** An error's message, or the thrown value as a string when it isn't an `Error`. */
6
+ export declare const errorMessage: (error: unknown) => string;
7
+ /**
8
+ * A document, directory, or git's document listing that couldn't be read: the environment, not the
9
+ * documents or the config, stopped the check.
10
+ */
11
+ export declare class ReadError extends Error {
12
+ /** @param message What couldn't be read, and why. */
13
+ constructor(message: string);
14
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReadError = exports.errorMessage = exports.isSystemError = exports.isNotFoundError = void 0;
4
+ /** Whether a filesystem error means the path doesn't exist, as opposed to existing but being unreadable. */
5
+ const isNotFoundError = (error) => error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
6
+ exports.isNotFoundError = isNotFoundError;
7
+ /** Whether `error` is a Node system error (`EACCES`, `ELOOP`, ...) from a failed system call. */
8
+ const isSystemError = (error) => error instanceof Error && "code" in error && typeof error.code === "string" && "syscall" in error;
9
+ exports.isSystemError = isSystemError;
10
+ /** An error's message, or the thrown value as a string when it isn't an `Error`. */
11
+ const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
12
+ exports.errorMessage = errorMessage;
13
+ /**
14
+ * A document, directory, or git's document listing that couldn't be read: the environment, not the
15
+ * documents or the config, stopped the check.
16
+ */
17
+ class ReadError extends Error {
18
+ /** @param message What couldn't be read, and why. */
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "ReadError";
22
+ }
23
+ }
24
+ exports.ReadError = ReadError;
25
+ //# sourceMappingURL=fs-errors.js.map
@@ -0,0 +1,79 @@
1
+ import { type MarkdownNode } from "./parse";
2
+ /**
3
+ * The fragment ids a markdown document defines: upstream's anchors plus GitHub's. Upstream's are
4
+ * those `@eslint/markdown`'s `no-missing-link-fragments` computes for in-document `#anchor` links,
5
+ * so every fragment that passes there passes here too. GitHub's only add anchors, so they can only
6
+ * remove false positives.
7
+ *
8
+ * The document is parsed with GFM and YAML frontmatter, as `parseMarkdown` parses it (so a
9
+ * leading `---` block never yields a heading, while `+++` fences are plain markdown, as on GitHub),
10
+ * and one `github-slugger` per document slugs, in document order:
11
+ * - every heading (ATX or setext): the concatenated `value` of its non-HTML descendants, so
12
+ * inline code, emphasis, and link text count while image alt text and inline HTML don't; a
13
+ * trailing custom `{#id}` replaces the text;
14
+ * - in every HTML node, outside `<!-- comments -->`: each `id=`/`name=` attribute value, and the
15
+ * tag-stripped text of each `<h1>`-`<h6>` element.
16
+ * The slugger suffixes repeats (`setup`, `setup-1`, ...) across all of these, and `top` is always
17
+ * defined. `.mdx` targets are parsed with the MDX extensions too (`parseMarkdown` decides by the
18
+ * file extension): ESM and `{expressions}` yield no headings, markdown inside JSX children does, and
19
+ * a JSX element's string `id`/`name` attribute counts like the HTML one. An `{expression}` inside a
20
+ * heading adds no text, as in the ids Storybook's `rehype-slug` gives the rendered heading, so
21
+ * `# Hi {name}` defines `hi-` (the space before the expression stays), not `hi` or `hi-name`. A
22
+ * target that is not valid MDX, has MDX whose rendering parsing can't see (`knownDocumentAnchors`),
23
+ * or is larger than `MAX_ANCHOR_DOCUMENT_BYTES`, has no known anchors.
24
+ *
25
+ * GitHub's anchors, on top of those:
26
+ * - each explicit `id`/`name` value as written (lower-cased), since GitHub keeps `.` and `:`
27
+ * (`v1.2.0`, `a:b`) that slugging drops;
28
+ * - every markdown heading slugged by a second `github-slugger` of its own, over its full text
29
+ * with emoji shortcodes removed: GitHub has no `{#id}` (`## Setup {#install}` is `setup-install`)
30
+ * and renders `:rocket:` as an emoji the slug drops (`## :rocket: Deploy` is `-deploy`);
31
+ * - footnote ids as GitHub renders them: `fn-<label>` per definition, and `fnref-<label>`,
32
+ * `fnref-<label>-2`, ... per reference.
33
+ * `hasAnchor` also accepts each of these behind GitHub's `user-content-` prefix.
34
+ *
35
+ * `.mdx` documents get the same GFM and frontmatter reading, which deliberately differs from a
36
+ * Storybook build without those plugins: there `# Title[^1]` keeps `[^1]` as text (`title1`, not
37
+ * `title`), and a leading `---` block renders as a rule and a setext heading. So a `.mdx` document
38
+ * with frontmatter, or with a footnote reference inside a heading, has no known anchors
39
+ * (`knownDocumentAnchors`).
40
+ */
41
+ /**
42
+ * Documents above this size are skipped rather than parsed: a 1.1 MB CHANGELOG took 2 s and 417 MB
43
+ * of heap. A skipped target has no known anchors, so it is never reported.
44
+ */
45
+ export declare const MAX_ANCHOR_DOCUMENT_BYTES: number;
46
+ /**
47
+ * The fragment ids an already-parsed document defines (see the module comment): upstream's anchors
48
+ * (every HTML node's, and every heading's once its own HTML is collected), plus GitHub's.
49
+ */
50
+ export declare const documentAnchors: (root: MarkdownNode) => Set<string>;
51
+ /**
52
+ * The fragment ids a parsed document defines, as `documentAnchors` computes them, or `undefined`
53
+ * when MDX in it may render anchors that parsing can't see, so a caller never reports a fragment
54
+ * that the rendered page might define. That is any ESM (an import brings in components or content,
55
+ * an `export default` wraps the page in a layout); any JSX element that is a component (`<Meta>`,
56
+ * Storybook's `<Title />` and `<Stories />`, `<Markdown>{...}</Markdown>`) or a custom element
57
+ * (`<doc-toc>`); a JSX `<h1>`-`<h6>`, whose rendered id may differ between renderers (GitHub,
58
+ * Storybook); a spread or any `{expression}` attribute value; and any `{expression}` holding code
59
+ * (see `isCommentExpression`), which covers a heading like `# Hi {name}`. In a `.mdx` `file`,
60
+ * frontmatter and a footnote reference inside a heading count too (see the module comment).
61
+ * Built-in HTML elements with string attributes and comment-only expressions are seen in full.
62
+ * Plain markdown has none of these nodes, and its frontmatter and footnotes read as on GitHub, so a
63
+ * `.md` document's anchors are always known.
64
+ */
65
+ export declare const knownDocumentAnchors: (root: MarkdownNode, file: string) => Set<string> | undefined;
66
+ /**
67
+ * The fragment ids `absolutePath` defines (see the module comment), or `undefined` when it can't be
68
+ * read or parsed, is larger than `MAX_ANCHOR_DOCUMENT_BYTES`, or has MDX whose rendered anchors
69
+ * can't be seen (see `knownDocumentAnchors`), so a caller never reports against a document it
70
+ * didn't fully inspect.
71
+ */
72
+ export declare const readDocumentAnchors: (absolutePath: string) => ReadonlySet<string> | undefined;
73
+ /**
74
+ * Whether a written fragment names one of `anchors`, compared as `no-missing-link-fragments` does by
75
+ * default: percent-decoded (a malformed escape as written) and case-insensitively. GitHub line
76
+ * references (`L10`, `L10C2-L12C5`) always match, and so does an anchor behind the `user-content-`
77
+ * prefix GitHub gives the ids it renders (`#user-content-setup`).
78
+ */
79
+ export declare const hasAnchor: (anchors: ReadonlySet<string>, fragment: string) => boolean;