@tsdoctor/pages 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/Blocks.js +358 -0
- package/Build.js +525 -0
- package/Examples.js +237 -0
- package/LICENSE +21 -0
- package/Llms.js +282 -0
- package/Markdown.js +155 -0
- package/Nav.js +148 -0
- package/Page.js +80 -0
- package/README.md +32 -0
- package/Scope.js +56 -0
- package/TwoslashDirectives.js +63 -0
- package/WorkItems.js +162 -0
- package/index.d.ts +1193 -0
- package/index.js +12 -0
- package/package.json +54 -0
- package/tsdoc-metadata.json +11 -0
package/Nav.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/Nav.ts
|
|
4
|
+
/**
|
|
5
|
+
* The per-API navigation tree — category groups of pages plus the index —
|
|
6
|
+
* as data, so a sidebar that is files in one framework and config in another
|
|
7
|
+
* is a pure rendering of the same value.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The ordering is the one the RSPress adapter's `writeMetadata` produced when
|
|
11
|
+
* it wrote `_meta.json` files directly: groups in category insertion order,
|
|
12
|
+
* kept only when at least one page landed in them; pages within a group by
|
|
13
|
+
* `label.localeCompare`; the index page always present. `buildNav` is
|
|
14
|
+
* characterized against that behaviour, and the RSPress rendering of this
|
|
15
|
+
* tree is covered by the golden gate.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A page's place in the navigation tree, carried on the page itself.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var NavEntry = class extends Schema.Class("NavEntry")({
|
|
25
|
+
/** The category key the page was categorized under. */
|
|
26
|
+
categoryKey: Schema.String,
|
|
27
|
+
/** The sidebar label — the display name, qualified for a namespace member. */
|
|
28
|
+
label: Schema.String,
|
|
29
|
+
/** The file basename without extension (`foo` for `class/foo.mdx`). */
|
|
30
|
+
name: Schema.String,
|
|
31
|
+
/** The page route. */
|
|
32
|
+
route: Schema.String
|
|
33
|
+
}) {};
|
|
34
|
+
/**
|
|
35
|
+
* The per-category presentation facts a tree carries for its groups.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
var NavCategory = class extends Schema.Class("NavCategory")({
|
|
40
|
+
/** The group label. */
|
|
41
|
+
displayName: Schema.String,
|
|
42
|
+
/** The folder the category's pages live in. */
|
|
43
|
+
folderName: Schema.String,
|
|
44
|
+
/** Whether the group can be collapsed; absent means the renderer's default. */
|
|
45
|
+
collapsible: Schema.optionalKey(Schema.Boolean),
|
|
46
|
+
/** Whether the group starts collapsed; absent means the renderer's default. */
|
|
47
|
+
collapsed: Schema.optionalKey(Schema.Boolean),
|
|
48
|
+
/** Heading depths surfaced in an overview; absent means the renderer's default. */
|
|
49
|
+
overviewHeaders: Schema.optionalKey(Schema.Array(Schema.Number))
|
|
50
|
+
}) {};
|
|
51
|
+
/**
|
|
52
|
+
* One page in a group.
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
var NavPage = class extends Schema.Class("NavPage")({
|
|
57
|
+
/** The sidebar label. */
|
|
58
|
+
label: Schema.String,
|
|
59
|
+
/** The file basename without extension. */
|
|
60
|
+
name: Schema.String,
|
|
61
|
+
/** The page route. */
|
|
62
|
+
route: Schema.String
|
|
63
|
+
}) {};
|
|
64
|
+
/**
|
|
65
|
+
* One category group with its pages, already sorted.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
var NavGroup = class extends Schema.Class("NavGroup")({
|
|
70
|
+
/** The category key. */
|
|
71
|
+
key: Schema.String,
|
|
72
|
+
/** The category presentation facts. */
|
|
73
|
+
category: NavCategory,
|
|
74
|
+
/** The group's pages, sorted by label. */
|
|
75
|
+
pages: Schema.Array(NavPage)
|
|
76
|
+
}) {};
|
|
77
|
+
/**
|
|
78
|
+
* The navigation tree for one API.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
var NavTree = class extends Schema.Class("NavTree")({
|
|
83
|
+
/** The API's base route; the index page lives at its root. */
|
|
84
|
+
baseRoute: Schema.String,
|
|
85
|
+
/** The index page. */
|
|
86
|
+
index: NavPage,
|
|
87
|
+
/** The category groups that received at least one page, in category order. */
|
|
88
|
+
groups: Schema.Array(NavGroup)
|
|
89
|
+
}) {};
|
|
90
|
+
/**
|
|
91
|
+
* The label of the index page every tree carries.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
const NAV_INDEX_LABEL = "API Reference";
|
|
96
|
+
/**
|
|
97
|
+
* Sort pages the way the sidebar lists them: alphabetically by label.
|
|
98
|
+
*
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
function sortNavPages(pages) {
|
|
102
|
+
return [...pages].sort((a, b) => a.label.localeCompare(b.label));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the navigation tree for one API from its categories and its pages.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* Groups follow `categories`' insertion order and a category with no page
|
|
109
|
+
* is dropped rather than rendered empty. An entry whose category key names
|
|
110
|
+
* no configured category is dropped too — it could not have been generated
|
|
111
|
+
* into a folder — so the tree only ever describes pages that exist.
|
|
112
|
+
*
|
|
113
|
+
* @public
|
|
114
|
+
*/
|
|
115
|
+
function buildNav(input) {
|
|
116
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const entry of input.entries) {
|
|
118
|
+
const pages = byCategory.get(entry.categoryKey) ?? [];
|
|
119
|
+
pages.push(NavPage.make({
|
|
120
|
+
label: entry.label,
|
|
121
|
+
name: entry.name,
|
|
122
|
+
route: entry.route
|
|
123
|
+
}));
|
|
124
|
+
byCategory.set(entry.categoryKey, pages);
|
|
125
|
+
}
|
|
126
|
+
const groups = [];
|
|
127
|
+
for (const [key, category] of Object.entries(input.categories)) {
|
|
128
|
+
const pages = byCategory.get(key);
|
|
129
|
+
if (!pages || pages.length === 0) continue;
|
|
130
|
+
groups.push(NavGroup.make({
|
|
131
|
+
key,
|
|
132
|
+
category,
|
|
133
|
+
pages: sortNavPages(pages)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
return NavTree.make({
|
|
137
|
+
baseRoute: input.baseRoute,
|
|
138
|
+
index: NavPage.make({
|
|
139
|
+
label: NAV_INDEX_LABEL,
|
|
140
|
+
name: "index",
|
|
141
|
+
route: `${input.baseRoute}/index`
|
|
142
|
+
}),
|
|
143
|
+
groups
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { NAV_INDEX_LABEL, NavCategory, NavEntry, NavGroup, NavPage, NavTree, buildNav, sortNavPages };
|
package/Page.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Block } from "./Blocks.js";
|
|
2
|
+
import { NavEntry } from "./Nav.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/Page.ts
|
|
6
|
+
/**
|
|
7
|
+
* The neutral head tag, as a schema — the same shape as `@tsdoctor/seo`'s
|
|
8
|
+
* `HeadTag` interface, so a value from `headTags` is accepted unchanged.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const HeadTag = Schema.Struct({
|
|
13
|
+
/** The element name. */
|
|
14
|
+
tag: Schema.Literals([
|
|
15
|
+
"meta",
|
|
16
|
+
"link",
|
|
17
|
+
"script"
|
|
18
|
+
]),
|
|
19
|
+
/** The element attributes. */
|
|
20
|
+
attrs: Schema.Record(Schema.String, Schema.String),
|
|
21
|
+
/** Element content; only meaningful for `script`. */
|
|
22
|
+
body: Schema.optionalKey(Schema.String)
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* The kind of symbol a page documents — which builder produced it, and
|
|
26
|
+
* which component imports and block layout an emitter chooses.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
const PageKind = Schema.Literals([
|
|
31
|
+
"class",
|
|
32
|
+
"interface",
|
|
33
|
+
"function",
|
|
34
|
+
"type-alias",
|
|
35
|
+
"enum",
|
|
36
|
+
"variable",
|
|
37
|
+
"namespace"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* One generated API page.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
var Page = class extends Schema.Class("Page")({
|
|
45
|
+
/** The kind of symbol the page documents. */
|
|
46
|
+
kind: PageKind,
|
|
47
|
+
/** The documented item's display name — the first title part. */
|
|
48
|
+
entityName: Schema.String,
|
|
49
|
+
/** The category's singular name (`Class`, `Function`) — the second title part. */
|
|
50
|
+
singularName: Schema.String,
|
|
51
|
+
/** The API's display name — the last title part, when the site names one. */
|
|
52
|
+
apiName: Schema.optionalKey(Schema.String),
|
|
53
|
+
/** The page description: the item's summary, or the fallback the generators used. */
|
|
54
|
+
description: Schema.String,
|
|
55
|
+
/** The page route. */
|
|
56
|
+
route: Schema.String,
|
|
57
|
+
/** Every `<head>` tag the page carries, from `@tsdoctor/seo`. */
|
|
58
|
+
headTags: Schema.Array(HeadTag),
|
|
59
|
+
/** The page body, in order. */
|
|
60
|
+
blocks: Schema.Array(Block),
|
|
61
|
+
/** The page's place in the navigation tree. */
|
|
62
|
+
nav: NavEntry
|
|
63
|
+
}) {
|
|
64
|
+
/**
|
|
65
|
+
* The structured page title: `{entityName} | {singularName} | API | {apiName}`,
|
|
66
|
+
* the last part omitted when the site names no API.
|
|
67
|
+
*/
|
|
68
|
+
get title() {
|
|
69
|
+
const parts = [
|
|
70
|
+
this.entityName,
|
|
71
|
+
this.singularName,
|
|
72
|
+
"API"
|
|
73
|
+
];
|
|
74
|
+
if (this.apiName !== void 0) parts.push(this.apiName);
|
|
75
|
+
return parts.join(" | ");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { HeadTag, Page, PageKind };
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @tsdoctor/pages
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@tsdoctor/pages)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
The framework-neutral documentation page IR for static TypeScript API sites. A page is facts, an ordered list of typed doc blocks and its navigation entry; prose inside a block is `@effected/markdown` mdast, and code-bearing blocks carry a `display` / `source` pair. A framework adapter is an emitter over this IR.
|
|
9
|
+
|
|
10
|
+
## What you get
|
|
11
|
+
|
|
12
|
+
- **Blocks** — the block vocabulary (`Title`, `Signature`, `MemberGroup`, `ParameterTable`, `EnumMemberTable`, `ExampleGroup`, …) as Effect `Schema.Class` variants discriminated on `kind`, plus the `Block` union.
|
|
13
|
+
- **`Page`** — the page record: title parts, description, route, `HeadTag[]`, blocks and nav entry.
|
|
14
|
+
- **`buildNav`** — one `NavTree` per API (category groups, pages, index), sorted deterministically.
|
|
15
|
+
- **`buildExample`, `codeText`, `prepareExampleCode`, `stripTwoslashDirectives`, `prependHiddenImports`, `formatExampleCode`** — display/source preparation for code blocks, with Prettier formatting behind a typed `ExampleFormatError`.
|
|
16
|
+
- **`renderMarkdown` / `renderMarkdownResult`** — the neutral plain-markdown emitter over the IR.
|
|
17
|
+
- **`parseLlmsTxtLine`, `filterLlmsTxt`, `generateStructuredLlmsTxt`, …** — pure text transforms over the llms.txt standard.
|
|
18
|
+
- **`apiScopeOf`, `unscopedName`, `normalizeBaseRoute`** — API scope naming helpers shared by every adapter.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @tsdoctor/pages
|
|
24
|
+
# or
|
|
25
|
+
pnpm add @tsdoctor/pages
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires `effect` and `@effected/markdown` as peers.
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
package/Scope.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/Scope.ts
|
|
2
|
+
/**
|
|
3
|
+
* API scope naming — the helpers that turn a package name and a base route
|
|
4
|
+
* into the identifiers every adapter must agree on.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* These sit beside the navigation tree rather than in an adapter because the
|
|
8
|
+
* scope string is load-bearing across frameworks: it keys Twoslash cache
|
|
9
|
+
* generations and names the per-package llms files. Two adapters deriving it
|
|
10
|
+
* differently would silently miss each other's caches. The multiVersion /
|
|
11
|
+
* i18n output-directory layout is NOT here — that is a framework's product
|
|
12
|
+
* policy and stays adapter-side.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Extract the unscoped name from a possibly scoped package name:
|
|
18
|
+
* `@scope/pkg` becomes `pkg`, `pkg` stays `pkg`.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
function unscopedName(packageName) {
|
|
23
|
+
return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a base route: ensure a leading slash, strip a trailing slash,
|
|
27
|
+
* and keep the root as `/`.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
function normalizeBaseRoute(route) {
|
|
32
|
+
const withSlash = route.startsWith("/") ? route : `/${route}`;
|
|
33
|
+
const stripped = withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
|
|
34
|
+
return stripped === "" ? "/" : stripped;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The API scope key derived from a base route: its first path segment,
|
|
38
|
+
* falling back to the package name so a single-API site mounted at `/`
|
|
39
|
+
* still gets a non-empty scope.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Load-bearing and previously duplicated. Config resolution registers each
|
|
43
|
+
* API's Twoslash environment under this key and the build program looks it
|
|
44
|
+
* up by the same key; if two derivations disagree, every lookup misses and
|
|
45
|
+
* per-scope type-checking degrades to build-wide with no error and nothing
|
|
46
|
+
* visibly wrong in the output. One definition matters more than the
|
|
47
|
+
* duplication was costing.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
function apiScopeOf(baseRoute, packageName) {
|
|
52
|
+
return baseRoute.replace(/^\//, "").split("/")[0] || packageName;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { apiScopeOf, normalizeBaseRoute, unscopedName };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/TwoslashDirectives.ts
|
|
2
|
+
/**
|
|
3
|
+
* Twoslash directive detection — the regexes that decide which lines of a
|
|
4
|
+
* code block are notation rather than code.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* These mirror the upstream Twoslash source
|
|
8
|
+
* (`twoslashes/twoslash`, `packages/twoslash/src/regexp.ts`). All patterns
|
|
9
|
+
* allow an optional space after `//`, so both `// @noErrors` and `//@noErrors`
|
|
10
|
+
* are recognized, as Twoslash itself does. They live in the IR package
|
|
11
|
+
* because the display/source split depends on them and both adapters must
|
|
12
|
+
* strip the same lines.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Config directives: boolean flags and key-value pairs.
|
|
18
|
+
*
|
|
19
|
+
* Upstream: `reConfigBoolean` + `reConfigValue` + `reFilenamesMakers` —
|
|
20
|
+
* `// @noErrors`, `//@strict`, `// @errors: 2304`, `// @filename: example.ts`.
|
|
21
|
+
*/
|
|
22
|
+
const RE_CONFIG = /^\/\/\s?@\w+/;
|
|
23
|
+
/**
|
|
24
|
+
* Annotation markers: query, completion, and highlight markers.
|
|
25
|
+
*
|
|
26
|
+
* Upstream: `reAnnonateMarkers` — `/^\s*\/\/\s*\^(\?|\||\^+)( .*)?$/gm`. After
|
|
27
|
+
* `line.trim()` leading whitespace is gone but the spaces between `//` and
|
|
28
|
+
* `^` are preserved, so `// ^?` still matches.
|
|
29
|
+
*/
|
|
30
|
+
const RE_ANNOTATION = /^\/\/\s*\^[?|^]/;
|
|
31
|
+
/**
|
|
32
|
+
* Cut directives: `// ---cut---`, `//---cut-before---`, `// ---cut-after---`,
|
|
33
|
+
* `// ---cut-start---`, `// ---cut-end---`.
|
|
34
|
+
*/
|
|
35
|
+
const RE_CUT = /^\/\/\s?---cut/;
|
|
36
|
+
/**
|
|
37
|
+
* Test whether a trimmed line is any Twoslash directive — a config flag or
|
|
38
|
+
* value, a filename marker, an annotation marker or a cut directive.
|
|
39
|
+
*
|
|
40
|
+
* @param trimmedLine - The line with leading and trailing whitespace removed
|
|
41
|
+
* @returns `true` if the line is a Twoslash directive
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
function isTwoslashDirective(trimmedLine) {
|
|
45
|
+
return RE_CONFIG.test(trimmedLine) || RE_ANNOTATION.test(trimmedLine) || RE_CUT.test(trimmedLine);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify a cut directive line.
|
|
49
|
+
*
|
|
50
|
+
* @param trimmedLine - The line with leading and trailing whitespace removed
|
|
51
|
+
* @returns The cut form, or `null` if the line is not a cut directive
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
function classifyCutDirective(trimmedLine) {
|
|
55
|
+
if (/^\/\/\s?---cut(-before)?---$/.test(trimmedLine)) return "cut-before";
|
|
56
|
+
if (/^\/\/\s?---cut-after---$/.test(trimmedLine)) return "cut-after";
|
|
57
|
+
if (/^\/\/\s?---cut-start---$/.test(trimmedLine)) return "cut-start";
|
|
58
|
+
if (/^\/\/\s?---cut-end---$/.test(trimmedLine)) return "cut-end";
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { classifyCutDirective, isTwoslashDirective };
|
package/WorkItems.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ApiItems, EntryPoints, Routes, SyntheticBases } from "@tsdoctor/model";
|
|
2
|
+
|
|
3
|
+
//#region src/WorkItems.ts
|
|
4
|
+
/**
|
|
5
|
+
* Cross-link priority by API item kind (lower = higher priority). When a bare
|
|
6
|
+
* name maps to multiple pages (the const+type companion pattern), the bare
|
|
7
|
+
* cross-link resolves to the higher-priority kind — value declarations win
|
|
8
|
+
* over type-only declarations, so `Foo` links to the importable schema, not
|
|
9
|
+
* the type.
|
|
10
|
+
*/
|
|
11
|
+
const CROSS_LINK_KIND_PRIORITY = {
|
|
12
|
+
Class: 0,
|
|
13
|
+
Function: 1,
|
|
14
|
+
Variable: 2,
|
|
15
|
+
Enum: 3,
|
|
16
|
+
Interface: 4,
|
|
17
|
+
TypeAlias: 5,
|
|
18
|
+
Namespace: 6
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Lower number = higher priority for which page a bare cross-link name
|
|
22
|
+
* resolves to.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
function crossLinkKindPriority(kind) {
|
|
27
|
+
return CROSS_LINK_KIND_PRIORITY[kind] ?? 100;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Prepare the flat list of work items to build and the cross-link maps.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Resolves entry points into deduplicated items, detects synthetic base
|
|
34
|
+
* declarations (excluded from categorization, collision detection and work
|
|
35
|
+
* items — the owner class page renders them inline), categorizes, detects
|
|
36
|
+
* route collisions on the lowercased `folder/name` route, builds the route
|
|
37
|
+
* map with bare names owned by the highest-priority kind and member routes
|
|
38
|
+
* from the model's anchors, adds namespace member routes (qualified always,
|
|
39
|
+
* unqualified PascalCase when unambiguous), routes synthetic base names to
|
|
40
|
+
* the owner's `#base-class` anchor, and flattens everything into work items.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
function prepareWorkItems(input) {
|
|
45
|
+
const { apiPackage, categories, baseRoute } = input;
|
|
46
|
+
const resolvedItems = EntryPoints.resolve(apiPackage);
|
|
47
|
+
const syntheticBases = SyntheticBases.detect(resolvedItems.map((r) => r.item));
|
|
48
|
+
const docItems = syntheticBases.bases.size ? resolvedItems.filter((r) => !syntheticBases.bases.has(r.item)) : resolvedItems;
|
|
49
|
+
const resolvedLookup = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const resolved of docItems) resolvedLookup.set(`${resolved.item.displayName}::${resolved.item.kind}`, resolved);
|
|
51
|
+
const { items, uncategorized } = ApiItems.categorize(docItems, categories);
|
|
52
|
+
const namespaceMembers = ApiItems.namespaceMembers(docItems);
|
|
53
|
+
const categoryFor = (item) => Object.entries(categories).find(([, config]) => config.itemKinds?.includes(item.kind));
|
|
54
|
+
const candidates = [];
|
|
55
|
+
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push(new Routes.RouteCandidate({
|
|
56
|
+
id: `${item.displayName}::${item.kind}`,
|
|
57
|
+
displayName: item.displayName,
|
|
58
|
+
folder: categoryConfig.folderName,
|
|
59
|
+
baseName: item.displayName.toLowerCase(),
|
|
60
|
+
kind: String(item.kind),
|
|
61
|
+
canonicalRef: item.canonicalReference?.toString() ?? item.displayName
|
|
62
|
+
}));
|
|
63
|
+
for (const nsMember of namespaceMembers) {
|
|
64
|
+
const nsCategory = categoryFor(nsMember.item);
|
|
65
|
+
if (!nsCategory) continue;
|
|
66
|
+
candidates.push(new Routes.RouteCandidate({
|
|
67
|
+
id: nsMember.qualifiedName,
|
|
68
|
+
displayName: nsMember.qualifiedName,
|
|
69
|
+
folder: nsCategory[1].folderName,
|
|
70
|
+
baseName: nsMember.qualifiedName.toLowerCase(),
|
|
71
|
+
kind: String(nsMember.item.kind),
|
|
72
|
+
canonicalRef: nsMember.item.canonicalReference?.toString() ?? nsMember.qualifiedName
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const collisions = Routes.detectCollisions(candidates);
|
|
76
|
+
const routes = /* @__PURE__ */ new Map();
|
|
77
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
78
|
+
const routeOwnerPriority = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) {
|
|
80
|
+
const itemRoute = `${baseRoute}/${categoryConfig.folderName}/${item.displayName.toLowerCase()}`;
|
|
81
|
+
const priority = crossLinkKindPriority(String(item.kind));
|
|
82
|
+
const existingPriority = routeOwnerPriority.get(item.displayName);
|
|
83
|
+
if (existingPriority === void 0 || priority < existingPriority) {
|
|
84
|
+
routes.set(item.displayName, itemRoute);
|
|
85
|
+
kinds.set(item.displayName, item.kind);
|
|
86
|
+
routeOwnerPriority.set(item.displayName, priority);
|
|
87
|
+
}
|
|
88
|
+
if (item.kind === "Class" || item.kind === "Interface") {
|
|
89
|
+
const itemWithMembers = item;
|
|
90
|
+
const anchors = ApiItems.memberAnchors(itemWithMembers);
|
|
91
|
+
const byCanonicalRef = new Map(itemWithMembers.members.map((member) => [member.canonicalReference?.toString() ?? member.displayName, member]));
|
|
92
|
+
for (const [routeKey, memberId] of ApiItems.memberRouteKeys(itemWithMembers)) {
|
|
93
|
+
const member = byCanonicalRef.get(memberId);
|
|
94
|
+
if (!member) continue;
|
|
95
|
+
const anchor = anchors.get(memberId) ?? Routes.memberAnchor(member.displayName);
|
|
96
|
+
routes.set(routeKey, `${itemRoute}#${anchor}`);
|
|
97
|
+
kinds.set(routeKey, member.kind);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const unqualifiedNameCounts = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const nsMember of namespaceMembers) {
|
|
103
|
+
const name = nsMember.item.displayName;
|
|
104
|
+
unqualifiedNameCounts.set(name, (unqualifiedNameCounts.get(name) || 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
for (const nsMember of namespaceMembers) {
|
|
107
|
+
const category = categoryFor(nsMember.item);
|
|
108
|
+
if (!category) continue;
|
|
109
|
+
const qualifiedRoute = `${baseRoute}/${category[1].folderName}/${nsMember.qualifiedName.toLowerCase()}`;
|
|
110
|
+
routes.set(nsMember.qualifiedName, qualifiedRoute);
|
|
111
|
+
kinds.set(nsMember.qualifiedName, nsMember.item.kind);
|
|
112
|
+
const displayName = nsMember.item.displayName;
|
|
113
|
+
if (/^[A-Z]/.test(displayName) && (unqualifiedNameCounts.get(displayName) || 0) <= 1 && !routes.has(displayName)) {
|
|
114
|
+
routes.set(displayName, qualifiedRoute);
|
|
115
|
+
kinds.set(displayName, nsMember.item.kind);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const [baseItem, syntheticBase] of syntheticBases.bases) {
|
|
119
|
+
const baseName = baseItem.displayName;
|
|
120
|
+
if (routes.has(baseName)) continue;
|
|
121
|
+
const owner = syntheticBase.ownerClasses[0];
|
|
122
|
+
const ownerRoute = owner ? routes.get(owner.displayName) : void 0;
|
|
123
|
+
if (!ownerRoute) continue;
|
|
124
|
+
routes.set(baseName, `${ownerRoute}#${SyntheticBases.BASE_CLASS_ANCHOR}`);
|
|
125
|
+
kinds.set(baseName, baseItem.kind);
|
|
126
|
+
}
|
|
127
|
+
const workItems = [];
|
|
128
|
+
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) {
|
|
129
|
+
const resolved = resolvedLookup.get(`${item.displayName}::${item.kind}`);
|
|
130
|
+
const syntheticBase = syntheticBases.baseByOwner.get(item);
|
|
131
|
+
const memberAnchors = item.kind === "Class" || item.kind === "Interface" ? ApiItems.memberAnchors(item) : void 0;
|
|
132
|
+
workItems.push({
|
|
133
|
+
item,
|
|
134
|
+
categoryKey,
|
|
135
|
+
categoryConfig,
|
|
136
|
+
...resolved?.availableFrom != null ? { availableFrom: resolved.availableFrom } : {},
|
|
137
|
+
...syntheticBase != null ? { syntheticBase } : {},
|
|
138
|
+
...memberAnchors != null ? { memberAnchors } : {}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const nsMember of namespaceMembers) {
|
|
142
|
+
const category = categoryFor(nsMember.item);
|
|
143
|
+
if (category) workItems.push({
|
|
144
|
+
item: nsMember.item,
|
|
145
|
+
categoryKey: category[0],
|
|
146
|
+
categoryConfig: category[1],
|
|
147
|
+
namespaceMember: nsMember
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
workItems,
|
|
152
|
+
crossLinkData: {
|
|
153
|
+
routes,
|
|
154
|
+
kinds
|
|
155
|
+
},
|
|
156
|
+
uncategorized,
|
|
157
|
+
collisions
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { crossLinkKindPriority, prepareWorkItems };
|