@tsdoctor/model 0.2.2 → 0.4.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/ApiItems.js +58 -1
- package/CrossLinker.js +20 -2
- package/Model.js +1 -1
- package/README.md +3 -2
- package/Render.js +1 -1
- package/Routes.js +158 -3
- package/index.d.ts +133 -27
- package/index.js +2 -3
- package/internal/prose.js +1 -1
- package/package.json +2 -2
- package/StructuredData.js +0 -18
package/ApiItems.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { memberAnchors as memberAnchors$1, memberRouteKeys as memberRouteKeys$1 } from "./Routes.js";
|
|
2
3
|
import { hasModifier } from "./Tsdoc.js";
|
|
3
4
|
import { ApiItemKind } from "@microsoft/api-extractor-model";
|
|
4
5
|
|
|
@@ -6,6 +7,8 @@ import { ApiItemKind } from "@microsoft/api-extractor-model";
|
|
|
6
7
|
var ApiItems_exports = /* @__PURE__ */ __exportAll({
|
|
7
8
|
categorize: () => categorize,
|
|
8
9
|
inheritance: () => inheritance,
|
|
10
|
+
memberAnchors: () => memberAnchors,
|
|
11
|
+
memberRouteKeys: () => memberRouteKeys,
|
|
9
12
|
namespaceMembers: () => namespaceMembers,
|
|
10
13
|
sourceLink: () => sourceLink
|
|
11
14
|
});
|
|
@@ -106,6 +109,60 @@ function sourceLink(item, target) {
|
|
|
106
109
|
const baseUrl = `${target.url}/${ref}`;
|
|
107
110
|
return lineNumber ? `${baseUrl}/${filePath}#L${lineNumber}` : `${baseUrl}/${filePath}`;
|
|
108
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The slot a class member occupies, for anchor disambiguation.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* Getters and setters are `Method` items whose display name carries the
|
|
117
|
+
* `get `/`set ` prefix — that is how API Extractor models them.
|
|
118
|
+
*/
|
|
119
|
+
function memberSlot(member) {
|
|
120
|
+
const isStatic = member.isStatic === true;
|
|
121
|
+
if (member.kind === "Method" && (member.displayName.startsWith("get ") || member.displayName.startsWith("set "))) return "getter";
|
|
122
|
+
const isMethod = member.kind === "Method" || member.kind === "MethodSignature";
|
|
123
|
+
if (isStatic) return isMethod ? "static-method" : "static-property";
|
|
124
|
+
return isMethod ? "instance-method" : "instance-property";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Anchor id for every member of a class or interface, keyed by the member's
|
|
128
|
+
* canonical reference.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* The ONE place an API item is turned into the {@link Routes.MemberRef} shape
|
|
132
|
+
* anchor computation needs. Both the cross-link route map and the rendered
|
|
133
|
+
* page call this, so a member's `#fragment` and its `id=` cannot disagree —
|
|
134
|
+
* which they did before Task 1.1, when each side derived anchors separately.
|
|
135
|
+
*
|
|
136
|
+
* A canonical reference already distinguishes a static member from an
|
|
137
|
+
* instance member of the same name (`Foo.bar` vs `Foo#bar`), so it is the
|
|
138
|
+
* natural key; a member without one falls back to its display name.
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
142
|
+
function memberAnchors(item) {
|
|
143
|
+
return memberAnchors$1(memberRefs(item));
|
|
144
|
+
}
|
|
145
|
+
/** The {@link Routes.MemberRef} view of a class or interface's members. */
|
|
146
|
+
function memberRefs(item) {
|
|
147
|
+
return item.members.map((member) => ({
|
|
148
|
+
id: member.canonicalReference?.toString() ?? member.displayName,
|
|
149
|
+
displayName: member.displayName,
|
|
150
|
+
slot: memberSlot(member)
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Cross-link keys for a class or interface's members, mapped to the member's
|
|
155
|
+
* canonical reference.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* The {@link Routes.memberRouteKeys} vocabulary over real API items — see
|
|
159
|
+
* there for which keys are emitted and why `Class#member` is not one of them.
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*/
|
|
163
|
+
function memberRouteKeys(item) {
|
|
164
|
+
return memberRouteKeys$1(item.displayName, memberRefs(item));
|
|
165
|
+
}
|
|
109
166
|
|
|
110
167
|
//#endregion
|
|
111
|
-
export { ApiItems_exports, categorize, inheritance, namespaceMembers, sourceLink };
|
|
168
|
+
export { ApiItems_exports, categorize, inheritance, memberAnchors, memberRouteKeys, namespaceMembers, sourceLink };
|
package/CrossLinker.js
CHANGED
|
@@ -10,6 +10,24 @@ import { escapeRegExp } from "./internal/text.js";
|
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
+
* A word-boundary-anchored pattern for a literal name.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* `\b` is an assertion ABOUT the adjacent character, not a delimiter: after a
|
|
17
|
+
* non-word character it matches only when a word character follows. A key in
|
|
18
|
+
* TSDoc selector form — `Registry.(create:instance)` — ends in `)`, so a
|
|
19
|
+
* trailing `\b` made it unmatchable in every realistic sentence position
|
|
20
|
+
* ("See Registry.(create:instance) for details." did not match). Escaping was
|
|
21
|
+
* never the problem; the boundary was. Names that end in a word character —
|
|
22
|
+
* every plain identifier and every `Class.member` key — get the same `\b`
|
|
23
|
+
* they always did, so this is a strict widening.
|
|
24
|
+
*/
|
|
25
|
+
const boundedPattern = (name) => {
|
|
26
|
+
const lead = /^\w/.test(name) ? "\\b" : "";
|
|
27
|
+
const trail = /\w$/.test(name) ? "\\b" : "(?!\\w)";
|
|
28
|
+
return `${lead}${escapeRegExp(name)}${trail}`;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
13
31
|
* Links known API item names in prose to their documentation routes. Matching
|
|
14
32
|
* is longest-name-first with word boundaries, skipping code spans and existing
|
|
15
33
|
* links.
|
|
@@ -47,7 +65,7 @@ var CrossLinker = class CrossLinker {
|
|
|
47
65
|
for (const name of this.orderedNames) {
|
|
48
66
|
const route = this.routesByName.get(name);
|
|
49
67
|
if (route === void 0) continue;
|
|
50
|
-
const regex = new RegExp(
|
|
68
|
+
const regex = new RegExp(boundedPattern(name), "g");
|
|
51
69
|
result = result.replace(regex, (match, offset) => {
|
|
52
70
|
const before = result.slice(0, offset);
|
|
53
71
|
if (before.endsWith("](") || before.endsWith("[")) return match;
|
|
@@ -66,7 +84,7 @@ var CrossLinker = class CrossLinker {
|
|
|
66
84
|
for (const name of this.orderedNames) {
|
|
67
85
|
const route = this.routesByName.get(name);
|
|
68
86
|
if (route === void 0) continue;
|
|
69
|
-
const regex = new RegExp(
|
|
87
|
+
const regex = new RegExp(`${boundedPattern(name)}(?![a-zA-Z])`, "g");
|
|
70
88
|
result = result.replace(regex, (match, offset) => {
|
|
71
89
|
const beforeMatch = result.substring(0, offset);
|
|
72
90
|
if (beforeMatch.includes("<a") && !beforeMatch.includes("</a>")) return match;
|
package/Model.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { ApiModel } from "@microsoft/api-extractor-model";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
|
-
import { Effect, Schema } from "effect";
|
|
6
6
|
|
|
7
7
|
//#region src/Model.ts
|
|
8
8
|
/**
|
package/README.md
CHANGED
|
@@ -92,9 +92,10 @@ Supplying `filter` fully replaces the default, so compose it with `Render.isEmit
|
|
|
92
92
|
|
|
93
93
|
- **`Model`** — Effect-typed `.api.json` loading. `Model.load(path)` returns the package's `ApiPackage` or fails with `ModelNotFoundError` / `ModelParseError`; `Model.firstPackage(apiModel)` extracts a package from a caller-constructed `ApiModel`, failing with `EmptyModelError` if it has none.
|
|
94
94
|
- **`Tsdoc`** — pure extraction off an `ApiItem`: `summary`, `params`, `returns`, `examples`, `deprecation`, `releaseTag`, `hasModifier`, `seeReferences`, plus `plainText`/`toMarkdown` for walking a raw TSDoc `DocNode` tree yourself.
|
|
95
|
-
- **`ApiItems`** — `categorize(items, categories)` groups top-level items by category key (returning `{ items, uncategorized }` so the caller decides how to handle the leftovers), `namespaceMembers` flattens namespace contents with qualified names, `inheritance` reads extends/implements, `sourceLink` builds a source-code URL.
|
|
95
|
+
- **`ApiItems`** — `categorize(items, categories)` groups top-level items by category key (returning `{ items, uncategorized }` so the caller decides how to handle the leftovers), `namespaceMembers` flattens namespace contents with qualified names, `inheritance` reads extends/implements, `sourceLink` builds a source-code URL; `memberAnchors(item)` and `memberRouteKeys(item)` are the `ApiClass` / `ApiInterface` views of the `Routes` functions below.
|
|
96
96
|
- **`EntryPoints`** — `resolve(apiPackage)` deduplicates items re-exported from more than one entry point (e.g. `.` and `./testing`) into a flat list, recording every entry point each item is available from.
|
|
97
|
-
- **`Routes`** — `RouteCandidate`, `detectCollisions`, and the typed `RouteCollisionError` for failing a build when two distinct items would resolve to the same output route; `sanitizeId` is the single anchor-id sanitizer for member routes.
|
|
97
|
+
- **`Routes`** — `RouteCandidate`, `detectCollisions`, and the typed `RouteCollisionError` for failing a build when two distinct items would resolve to the same output route; `sanitizeId` is the single anchor-id sanitizer for member routes, with `memberAnchor` as its named alias for one member.
|
|
98
|
+
- **`Routes` member anchors and keys** — `memberAnchors(members)` computes every member's anchor in one pass, keyed by `MemberRef.id`, prefixing the lower-priority member when two sanitize alike (`static create()` keeps `#create`, the instance one becomes `#instance-create`). `memberRouteKeys(className, members)` returns the cross-link keys those anchors answer to: a bare `Class.member` resolves to the static member when a class has both, with `Class.(member:static)`, `Class.(member:instance)` and `Class.prototype.member` emitted to disambiguate. Feed both from the same member list so a page's `id=` and a link's `#fragment` cannot drift.
|
|
98
99
|
- **`SyntheticBases`** — `detect(items)` finds the unexported `*_base` declarations an exported class's `extends` clause references, so an adapter can inline them instead of generating (or silently dropping) a page for them; `BASE_CLASS_ANCHOR` is the matching anchor id.
|
|
99
100
|
- **`Signature`** — `format(excerpt)` turns an API Extractor `Excerpt` into a clean, line-wrapped type signature string; `stripExportDeclare` strips `export`/`declare` modifiers from declaration text.
|
|
100
101
|
- **`CrossLinker`** — an immutable class that wraps known item names in prose with links, skipping code spans and existing links. Build one per build from a precomputed route map (`CrossLinker.fromRoutes`) or from item refs plus an injected URL scheme (`CrossLinker.fromRefs`); `link` returns markdown links, `linkHtml` returns `<a>` anchors.
|
package/Render.js
CHANGED
|
@@ -4,8 +4,8 @@ import { CrossLinker } from "./CrossLinker.js";
|
|
|
4
4
|
import { phrasingFromMarkdown } from "./internal/prose.js";
|
|
5
5
|
import { format } from "./Signature.js";
|
|
6
6
|
import { ApiItemContainerMixin } from "@microsoft/api-extractor-model";
|
|
7
|
-
import { Blockquote, Code, Heading, InlineCode, List, ListItem, Markdown, Paragraph, Root, Strong, Text } from "@effected/markdown";
|
|
8
7
|
import { Result } from "effect";
|
|
8
|
+
import { Blockquote, Code, Heading, InlineCode, List, ListItem, Markdown, Paragraph, Root, Strong, Text } from "@effected/markdown";
|
|
9
9
|
|
|
10
10
|
//#region src/Render.ts
|
|
11
11
|
var Render_exports = /* @__PURE__ */ __exportAll({
|
package/Routes.js
CHANGED
|
@@ -13,6 +13,9 @@ var Routes_exports = /* @__PURE__ */ __exportAll({
|
|
|
13
13
|
RouteCandidate: () => RouteCandidate,
|
|
14
14
|
RouteCollisionError: () => RouteCollisionError,
|
|
15
15
|
detectCollisions: () => detectCollisions,
|
|
16
|
+
memberAnchor: () => memberAnchor,
|
|
17
|
+
memberAnchors: () => memberAnchors,
|
|
18
|
+
memberRouteKeys: () => memberRouteKeys,
|
|
16
19
|
sanitizeId: () => sanitizeId
|
|
17
20
|
});
|
|
18
21
|
/**
|
|
@@ -96,8 +99,17 @@ var RouteCollisionError = class extends Schema.TaggedError()("RouteCollisionErro
|
|
|
96
99
|
/**
|
|
97
100
|
* Sanitize a display name into a valid HTML anchor id: lowercase,
|
|
98
101
|
* spaces/underscores → hyphens, other specials stripped, optional prefix for
|
|
99
|
-
* disambiguation.
|
|
100
|
-
*
|
|
102
|
+
* disambiguation.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The ONE canonical implementation. This docstring made that claim before it
|
|
106
|
+
* was true: the RSPress adapter carried a second, subtly different sanitizer
|
|
107
|
+
* for page-side `id=` attributes (it kept `_`, being in `\w`, and mapped `$`
|
|
108
|
+
* to `-`), so `get_value` was linked as `#get-value` and rendered as
|
|
109
|
+
* `id="get_value"` — a cross-link that landed nowhere. Both sides call this
|
|
110
|
+
* now. Do not add a second spelling; if page ids ever genuinely need
|
|
111
|
+
* different treatment from route anchors, that is a design change, not a
|
|
112
|
+
* local helper.
|
|
101
113
|
*
|
|
102
114
|
* @public
|
|
103
115
|
*/
|
|
@@ -105,6 +117,149 @@ function sanitizeId(displayName, prefix = "") {
|
|
|
105
117
|
const sanitized = displayName.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
106
118
|
return prefix ? `${prefix}-${sanitized}` : sanitized;
|
|
107
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The anchor for a single member, given an already-decided prefix.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* A thin alias over {@link sanitizeId} that names the intent at the call site.
|
|
125
|
+
* Prefer {@link memberAnchors} when rendering a whole class — deciding the
|
|
126
|
+
* prefix per member is the part that is easy to get wrong.
|
|
127
|
+
*
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
function memberAnchor(displayName, prefix = "") {
|
|
131
|
+
return sanitizeId(displayName, prefix);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Slots ordered by which keeps the bare anchor when names collide.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Static slots lead because the bare cross-link key `Class.member` canonically
|
|
138
|
+
* means the static member (see {@link memberRouteKeys}). One naming decision
|
|
139
|
+
* applied to both halves: the name that resolves to a member is the name that
|
|
140
|
+
* member's anchor uses.
|
|
141
|
+
*/
|
|
142
|
+
const SLOT_PRIORITY = [
|
|
143
|
+
"static-method",
|
|
144
|
+
"static-property",
|
|
145
|
+
"instance-method",
|
|
146
|
+
"getter",
|
|
147
|
+
"instance-property"
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* The prefix a losing slot is disambiguated with.
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* The prefix marks the NON-canonical side, so an instance member displaced by
|
|
154
|
+
* a static one becomes `instance-create`. TypeScript forbids two members of
|
|
155
|
+
* one class sharing a name within the same static-ness, so in practice a
|
|
156
|
+
* collision is exactly one static and one instance member and only
|
|
157
|
+
* `"instance"` is ever emitted; the static entries are a total-map fallback.
|
|
158
|
+
*/
|
|
159
|
+
const SLOT_PREFIX = {
|
|
160
|
+
"static-method": "static",
|
|
161
|
+
"static-property": "static",
|
|
162
|
+
"instance-method": "instance",
|
|
163
|
+
getter: "instance",
|
|
164
|
+
"instance-property": "instance"
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Compute the anchor for every member of one class, keyed by
|
|
168
|
+
* {@link MemberRef.id}.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* When several members sanitize to the same anchor, the highest-priority slot
|
|
172
|
+
* keeps the bare anchor and every other member is prefixed
|
|
173
|
+
* (`instance-create`). Priority runs static method, static property, instance
|
|
174
|
+
* method, getter, instance property — static first, so the anchor agrees with
|
|
175
|
+
* the bare cross-link key, which resolves to the static member.
|
|
176
|
+
*
|
|
177
|
+
* The per-MEMBER keying is load-bearing. A previous implementation keyed the
|
|
178
|
+
* prefix by sanitized NAME, so both halves of a `static create()` / `create()`
|
|
179
|
+
* collision looked up the same entry and both rendered
|
|
180
|
+
* `id="static-create"` — two elements sharing one HTML id, and the instance
|
|
181
|
+
* member displaced from the anchor its cross-link pointed at. Keying by
|
|
182
|
+
* member means the loser moves and the winner does not.
|
|
183
|
+
*
|
|
184
|
+
* @public
|
|
185
|
+
*/
|
|
186
|
+
function memberAnchors(members) {
|
|
187
|
+
const bySanitized = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const member of members) {
|
|
189
|
+
const base = sanitizeId(member.displayName);
|
|
190
|
+
const bucket = bySanitized.get(base);
|
|
191
|
+
if (bucket) bucket.push(member);
|
|
192
|
+
else bySanitized.set(base, [member]);
|
|
193
|
+
}
|
|
194
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const bucket of bySanitized.values()) {
|
|
196
|
+
const winner = bucket.length === 1 ? bucket[0] : [...bucket].sort((a, b) => SLOT_PRIORITY.indexOf(a.slot) - SLOT_PRIORITY.indexOf(b.slot))[0];
|
|
197
|
+
for (const member of bucket) {
|
|
198
|
+
const prefix = member === winner ? "" : SLOT_PREFIX[member.slot];
|
|
199
|
+
anchors.set(member.id, memberAnchor(member.displayName, prefix));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return anchors;
|
|
203
|
+
}
|
|
204
|
+
/** Static slots, for the `:static` / `:instance` selector split. */
|
|
205
|
+
const STATIC_SLOTS = /* @__PURE__ */ new Set(["static-property", "static-method"]);
|
|
206
|
+
/**
|
|
207
|
+
* Cross-link keys for one class's members, mapped to the member they resolve
|
|
208
|
+
* to ({@link MemberRef.id}).
|
|
209
|
+
*
|
|
210
|
+
* @remarks
|
|
211
|
+
* The bare `Class.member` key resolves to the STATIC member when a class has
|
|
212
|
+
* both a static and an instance member of that name. `Registry.create` is the
|
|
213
|
+
* static access expression in TypeScript — the instance one is
|
|
214
|
+
* `registry.create` — so a prose author writing the qualified form means the
|
|
215
|
+
* static member.
|
|
216
|
+
*
|
|
217
|
+
* The disambiguating keys use TSDoc declaration-reference selectors, the
|
|
218
|
+
* vocabulary API Extractor canonical references already carry:
|
|
219
|
+
*
|
|
220
|
+
* - `Registry.create` — the static member (the common case)
|
|
221
|
+
* - `Registry.(create:instance)` — the instance member
|
|
222
|
+
* - `Registry.(create:static)` — the static member, explicitly
|
|
223
|
+
* - `Registry.prototype.create` — an alias for the instance member; real
|
|
224
|
+
* JavaScript rather than invented syntax, and what a reader guesses
|
|
225
|
+
*
|
|
226
|
+
* `Class#member` is deliberately NOT emitted. `#` is the URL fragment
|
|
227
|
+
* delimiter, so such a key reads ambiguously beside a route, and in modern
|
|
228
|
+
* TypeScript `#` denotes a PRIVATE field (`this.#count`) — the JSDoc
|
|
229
|
+
* convention predates both and has aged badly.
|
|
230
|
+
*
|
|
231
|
+
* Selector keys are emitted ONLY when a collision exists. On the
|
|
232
|
+
* overwhelmingly common class with no name collision the bare key is
|
|
233
|
+
* complete, and every extra key is one more pattern the prose cross-linker
|
|
234
|
+
* compiles and tests against every string it links.
|
|
235
|
+
*
|
|
236
|
+
* @public
|
|
237
|
+
*/
|
|
238
|
+
function memberRouteKeys(className, members) {
|
|
239
|
+
const byName = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const member of members) {
|
|
241
|
+
const bucket = byName.get(member.displayName);
|
|
242
|
+
if (bucket) bucket.push(member);
|
|
243
|
+
else byName.set(member.displayName, [member]);
|
|
244
|
+
}
|
|
245
|
+
const keys = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const [displayName, bucket] of byName) {
|
|
247
|
+
const statics = bucket.filter((m) => STATIC_SLOTS.has(m.slot));
|
|
248
|
+
const instances = bucket.filter((m) => !STATIC_SLOTS.has(m.slot));
|
|
249
|
+
const collides = statics.length > 0 && instances.length > 0;
|
|
250
|
+
const bare = statics[0] ?? instances[0];
|
|
251
|
+
if (bare) keys.set(`${className}.${displayName}`, bare.id);
|
|
252
|
+
if (!collides) continue;
|
|
253
|
+
const staticMember = statics[0];
|
|
254
|
+
const instanceMember = instances[0];
|
|
255
|
+
if (staticMember) keys.set(`${className}.(${displayName}:static)`, staticMember.id);
|
|
256
|
+
if (instanceMember) {
|
|
257
|
+
keys.set(`${className}.(${displayName}:instance)`, instanceMember.id);
|
|
258
|
+
keys.set(`${className}.prototype.${displayName}`, instanceMember.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return keys;
|
|
262
|
+
}
|
|
108
263
|
|
|
109
264
|
//#endregion
|
|
110
|
-
export { RouteCandidate, RouteCollisionError, Routes_exports, detectCollisions, sanitizeId };
|
|
265
|
+
export { RouteCandidate, RouteCollisionError, Routes_exports, detectCollisions, memberAnchor, memberAnchors, memberRouteKeys, sanitizeId };
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ApiClass, ApiInterface, ApiItem, ApiModel, ApiNamespace, ApiPackage, Excerpt } from "@microsoft/api-extractor-model";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
import { FlowContent, MarkdownNode } from "@effected/markdown";
|
|
4
|
-
import { PackageManifest } from "@effected/package-json";
|
|
5
4
|
import { DocNode } from "@microsoft/tsdoc";
|
|
6
5
|
declare namespace EntryPoints_d_exports {
|
|
7
6
|
export { ResolvedEntryItem, entryPointName, resolve };
|
|
@@ -45,7 +44,7 @@ declare function entryPointName(displayName: string): string;
|
|
|
45
44
|
*/
|
|
46
45
|
declare function resolve(apiPackage: ApiPackage): ResolvedEntryItem[];
|
|
47
46
|
declare namespace ApiItems_d_exports {
|
|
48
|
-
export { CategorizedItems, CategorySpec, Inheritance, NamespaceMember, SourceLinkTarget, categorize, inheritance, namespaceMembers, sourceLink };
|
|
47
|
+
export { CategorizedItems, CategorySpec, Inheritance, NamespaceMember, SourceLinkTarget, categorize, inheritance, memberAnchors$1 as memberAnchors, memberRouteKeys$1 as memberRouteKeys, namespaceMembers, sourceLink };
|
|
49
48
|
}
|
|
50
49
|
/**
|
|
51
50
|
* The category rules `categorize` reads — a structural subset of a consumer's
|
|
@@ -133,6 +132,34 @@ declare function inheritance(item: ApiClass | ApiInterface): Inheritance;
|
|
|
133
132
|
* @public
|
|
134
133
|
*/
|
|
135
134
|
declare function sourceLink(item: ApiItem, target?: SourceLinkTarget): string | null;
|
|
135
|
+
/**
|
|
136
|
+
* Anchor id for every member of a class or interface, keyed by the member's
|
|
137
|
+
* canonical reference.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* The ONE place an API item is turned into the {@link Routes.MemberRef} shape
|
|
141
|
+
* anchor computation needs. Both the cross-link route map and the rendered
|
|
142
|
+
* page call this, so a member's `#fragment` and its `id=` cannot disagree —
|
|
143
|
+
* which they did before Task 1.1, when each side derived anchors separately.
|
|
144
|
+
*
|
|
145
|
+
* A canonical reference already distinguishes a static member from an
|
|
146
|
+
* instance member of the same name (`Foo.bar` vs `Foo#bar`), so it is the
|
|
147
|
+
* natural key; a member without one falls back to its display name.
|
|
148
|
+
*
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
declare function memberAnchors$1(item: ApiClass | ApiInterface): ReadonlyMap<string, string>;
|
|
152
|
+
/**
|
|
153
|
+
* Cross-link keys for a class or interface's members, mapped to the member's
|
|
154
|
+
* canonical reference.
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* The {@link Routes.memberRouteKeys} vocabulary over real API items — see
|
|
158
|
+
* there for which keys are emitted and why `Class#member` is not one of them.
|
|
159
|
+
*
|
|
160
|
+
* @public
|
|
161
|
+
*/
|
|
162
|
+
declare function memberRouteKeys$1(item: ApiClass | ApiInterface): ReadonlyMap<string, string>;
|
|
136
163
|
//#endregion
|
|
137
164
|
//#region src/types.d.ts
|
|
138
165
|
/**
|
|
@@ -335,7 +362,7 @@ declare function item(apiItem: ApiItem, opts: RenderItemOptions): string;
|
|
|
335
362
|
*/
|
|
336
363
|
declare function docs(apiPackage: ApiPackage, opts: RenderPackageOptions): RenderedDoc[];
|
|
337
364
|
declare namespace Routes_d_exports {
|
|
338
|
-
export { RouteCandidate, RouteCollision, RouteCollisionError, detectCollisions, sanitizeId };
|
|
365
|
+
export { MemberRef, MemberSlot, RouteCandidate, RouteCollision, RouteCollisionError, detectCollisions, memberAnchor, memberAnchors, memberRouteKeys, sanitizeId };
|
|
339
366
|
}
|
|
340
367
|
declare const RouteCandidate_base: Schema.Class<RouteCandidate, Schema.Struct<{
|
|
341
368
|
/** Stable identity (e.g. `"displayName::kind"` or a namespace qualified name). */
|
|
@@ -404,12 +431,112 @@ declare class RouteCollisionError extends RouteCollisionError_base {
|
|
|
404
431
|
/**
|
|
405
432
|
* Sanitize a display name into a valid HTML anchor id: lowercase,
|
|
406
433
|
* spaces/underscores → hyphens, other specials stripped, optional prefix for
|
|
407
|
-
* disambiguation.
|
|
408
|
-
*
|
|
434
|
+
* disambiguation.
|
|
435
|
+
*
|
|
436
|
+
* @remarks
|
|
437
|
+
* The ONE canonical implementation. This docstring made that claim before it
|
|
438
|
+
* was true: the RSPress adapter carried a second, subtly different sanitizer
|
|
439
|
+
* for page-side `id=` attributes (it kept `_`, being in `\w`, and mapped `$`
|
|
440
|
+
* to `-`), so `get_value` was linked as `#get-value` and rendered as
|
|
441
|
+
* `id="get_value"` — a cross-link that landed nowhere. Both sides call this
|
|
442
|
+
* now. Do not add a second spelling; if page ids ever genuinely need
|
|
443
|
+
* different treatment from route anchors, that is a design change, not a
|
|
444
|
+
* local helper.
|
|
409
445
|
*
|
|
410
446
|
* @public
|
|
411
447
|
*/
|
|
412
448
|
declare function sanitizeId(displayName: string, prefix?: string): string;
|
|
449
|
+
/**
|
|
450
|
+
* Which slot a class member occupies. Two members may share a display name
|
|
451
|
+
* while occupying different slots (a `static create()` beside an instance
|
|
452
|
+
* `create()`), which is what {@link memberAnchors} disambiguates.
|
|
453
|
+
*
|
|
454
|
+
* @public
|
|
455
|
+
*/
|
|
456
|
+
type MemberSlot = "static-property" | "static-method" | "instance-property" | "instance-method" | "getter";
|
|
457
|
+
/**
|
|
458
|
+
* One class member, identified for anchor computation.
|
|
459
|
+
*
|
|
460
|
+
* @public
|
|
461
|
+
*/
|
|
462
|
+
interface MemberRef {
|
|
463
|
+
/**
|
|
464
|
+
* Caller-supplied stable identity, unique per member. An API Extractor
|
|
465
|
+
* `canonicalReference` is the natural choice: it already distinguishes a
|
|
466
|
+
* static member from an instance member of the same name (`Foo.bar` vs
|
|
467
|
+
* `Foo#bar`), so the caller never has to re-derive that distinction.
|
|
468
|
+
*/
|
|
469
|
+
readonly id: string;
|
|
470
|
+
/** The member's display name, as written in the source. */
|
|
471
|
+
readonly displayName: string;
|
|
472
|
+
/** The slot the member occupies. */
|
|
473
|
+
readonly slot: MemberSlot;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* The anchor for a single member, given an already-decided prefix.
|
|
477
|
+
*
|
|
478
|
+
* @remarks
|
|
479
|
+
* A thin alias over {@link sanitizeId} that names the intent at the call site.
|
|
480
|
+
* Prefer {@link memberAnchors} when rendering a whole class — deciding the
|
|
481
|
+
* prefix per member is the part that is easy to get wrong.
|
|
482
|
+
*
|
|
483
|
+
* @public
|
|
484
|
+
*/
|
|
485
|
+
declare function memberAnchor(displayName: string, prefix?: string): string;
|
|
486
|
+
/**
|
|
487
|
+
* Compute the anchor for every member of one class, keyed by
|
|
488
|
+
* {@link MemberRef.id}.
|
|
489
|
+
*
|
|
490
|
+
* @remarks
|
|
491
|
+
* When several members sanitize to the same anchor, the highest-priority slot
|
|
492
|
+
* keeps the bare anchor and every other member is prefixed
|
|
493
|
+
* (`instance-create`). Priority runs static method, static property, instance
|
|
494
|
+
* method, getter, instance property — static first, so the anchor agrees with
|
|
495
|
+
* the bare cross-link key, which resolves to the static member.
|
|
496
|
+
*
|
|
497
|
+
* The per-MEMBER keying is load-bearing. A previous implementation keyed the
|
|
498
|
+
* prefix by sanitized NAME, so both halves of a `static create()` / `create()`
|
|
499
|
+
* collision looked up the same entry and both rendered
|
|
500
|
+
* `id="static-create"` — two elements sharing one HTML id, and the instance
|
|
501
|
+
* member displaced from the anchor its cross-link pointed at. Keying by
|
|
502
|
+
* member means the loser moves and the winner does not.
|
|
503
|
+
*
|
|
504
|
+
* @public
|
|
505
|
+
*/
|
|
506
|
+
declare function memberAnchors(members: readonly MemberRef[]): ReadonlyMap<string, string>;
|
|
507
|
+
/**
|
|
508
|
+
* Cross-link keys for one class's members, mapped to the member they resolve
|
|
509
|
+
* to ({@link MemberRef.id}).
|
|
510
|
+
*
|
|
511
|
+
* @remarks
|
|
512
|
+
* The bare `Class.member` key resolves to the STATIC member when a class has
|
|
513
|
+
* both a static and an instance member of that name. `Registry.create` is the
|
|
514
|
+
* static access expression in TypeScript — the instance one is
|
|
515
|
+
* `registry.create` — so a prose author writing the qualified form means the
|
|
516
|
+
* static member.
|
|
517
|
+
*
|
|
518
|
+
* The disambiguating keys use TSDoc declaration-reference selectors, the
|
|
519
|
+
* vocabulary API Extractor canonical references already carry:
|
|
520
|
+
*
|
|
521
|
+
* - `Registry.create` — the static member (the common case)
|
|
522
|
+
* - `Registry.(create:instance)` — the instance member
|
|
523
|
+
* - `Registry.(create:static)` — the static member, explicitly
|
|
524
|
+
* - `Registry.prototype.create` — an alias for the instance member; real
|
|
525
|
+
* JavaScript rather than invented syntax, and what a reader guesses
|
|
526
|
+
*
|
|
527
|
+
* `Class#member` is deliberately NOT emitted. `#` is the URL fragment
|
|
528
|
+
* delimiter, so such a key reads ambiguously beside a route, and in modern
|
|
529
|
+
* TypeScript `#` denotes a PRIVATE field (`this.#count`) — the JSDoc
|
|
530
|
+
* convention predates both and has aged badly.
|
|
531
|
+
*
|
|
532
|
+
* Selector keys are emitted ONLY when a collision exists. On the
|
|
533
|
+
* overwhelmingly common class with no name collision the bare key is
|
|
534
|
+
* complete, and every extra key is one more pattern the prose cross-linker
|
|
535
|
+
* compiles and tests against every string it links.
|
|
536
|
+
*
|
|
537
|
+
* @public
|
|
538
|
+
*/
|
|
539
|
+
declare function memberRouteKeys(className: string, members: readonly MemberRef[]): ReadonlyMap<string, string>;
|
|
413
540
|
declare namespace Signature_d_exports {
|
|
414
541
|
export { FormatOptions, format, linkReferences, stripExportDeclare };
|
|
415
542
|
}
|
|
@@ -445,27 +572,6 @@ declare function format(excerpt: Excerpt, options?: FormatOptions): string;
|
|
|
445
572
|
* @public
|
|
446
573
|
*/
|
|
447
574
|
declare function linkReferences(text: string, excerpt: Excerpt, routesByCanonicalRef: ReadonlyMap<string, string>): string;
|
|
448
|
-
declare namespace StructuredData_d_exports {
|
|
449
|
-
export { StructuredDataGraph, derive };
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* A schema.org JSON-LD graph.
|
|
453
|
-
*
|
|
454
|
-
* @alpha
|
|
455
|
-
*/
|
|
456
|
-
interface StructuredDataGraph {
|
|
457
|
-
readonly "@context": "https://schema.org";
|
|
458
|
-
readonly "@graph": ReadonlyArray<Record<string, unknown>>;
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Derive schema.org structured data for a documented package.
|
|
462
|
-
*
|
|
463
|
-
* @remarks
|
|
464
|
-
* Not implemented yet — this is the phase-4 seam. Calling it throws.
|
|
465
|
-
*
|
|
466
|
-
* @alpha
|
|
467
|
-
*/
|
|
468
|
-
declare function derive(_apiPackage: ApiPackage, _manifest: PackageManifest): StructuredDataGraph;
|
|
469
575
|
declare namespace SyntheticBases_d_exports {
|
|
470
576
|
export { BASE_CLASS_ANCHOR, SyntheticBase, SyntheticBaseDetection, detect };
|
|
471
577
|
}
|
|
@@ -618,5 +724,5 @@ declare function seeReferences(item: ApiItem): ReadonlyArray<{
|
|
|
618
724
|
readonly text: string;
|
|
619
725
|
}>;
|
|
620
726
|
//#endregion
|
|
621
|
-
export { type ApiItemRef, ApiItems_d_exports as ApiItems, CrossLinker, type DocMeta, EntryPoints_d_exports as EntryPoints, type FrontmatterRenderer, type ItemKindSlug, Model_d_exports as Model, Render_d_exports as Render, type RenderPackageOptions, type RenderedDoc, type RouteFormatter, Routes_d_exports as Routes, Signature_d_exports as Signature,
|
|
727
|
+
export { type ApiItemRef, ApiItems_d_exports as ApiItems, CrossLinker, type DocMeta, EntryPoints_d_exports as EntryPoints, type FrontmatterRenderer, type ItemKindSlug, Model_d_exports as Model, Render_d_exports as Render, type RenderPackageOptions, type RenderedDoc, type RouteFormatter, Routes_d_exports as Routes, Signature_d_exports as Signature, SyntheticBases_d_exports as SyntheticBases, Tsdoc_d_exports as Tsdoc };
|
|
622
728
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Routes_exports } from "./Routes.js";
|
|
1
2
|
import { Tsdoc_exports } from "./Tsdoc.js";
|
|
2
3
|
import { ApiItems_exports } from "./ApiItems.js";
|
|
3
4
|
import { CrossLinker } from "./CrossLinker.js";
|
|
@@ -5,8 +6,6 @@ import { EntryPoints_exports } from "./EntryPoints.js";
|
|
|
5
6
|
import { Model_exports } from "./Model.js";
|
|
6
7
|
import { Signature_exports } from "./Signature.js";
|
|
7
8
|
import { Render_exports } from "./Render.js";
|
|
8
|
-
import { Routes_exports } from "./Routes.js";
|
|
9
|
-
import { StructuredData_exports } from "./StructuredData.js";
|
|
10
9
|
import { SyntheticBases_exports } from "./SyntheticBases.js";
|
|
11
10
|
|
|
12
|
-
export { ApiItems_exports as ApiItems, CrossLinker, EntryPoints_exports as EntryPoints, Model_exports as Model, Render_exports as Render, Routes_exports as Routes, Signature_exports as Signature,
|
|
11
|
+
export { ApiItems_exports as ApiItems, CrossLinker, EntryPoints_exports as EntryPoints, Model_exports as Model, Render_exports as Render, Routes_exports as Routes, Signature_exports as Signature, SyntheticBases_exports as SyntheticBases, Tsdoc_exports as Tsdoc };
|
package/internal/prose.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsdoctor/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Render Microsoft API Extractor models into LLM-lean markdown. Pure model loading, TSDoc extraction, type-signature formatting, and per-item markdown rendering.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@effected/markdown": "^0.7.0",
|
|
46
|
-
"@effected/package-json": "^0.
|
|
46
|
+
"@effected/package-json": "^0.13.0",
|
|
47
47
|
"effect": "4.0.0-rc.109"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
package/StructuredData.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
-
|
|
3
|
-
//#region src/StructuredData.ts
|
|
4
|
-
var StructuredData_exports = /* @__PURE__ */ __exportAll({ derive: () => derive });
|
|
5
|
-
/**
|
|
6
|
-
* Derive schema.org structured data for a documented package.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* Not implemented yet — this is the phase-4 seam. Calling it throws.
|
|
10
|
-
*
|
|
11
|
-
* @alpha
|
|
12
|
-
*/
|
|
13
|
-
function derive(_apiPackage, _manifest) {
|
|
14
|
-
throw new Error("@tsdoctor/model: StructuredData.derive is a phase-4 seam and is not implemented yet");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
//#endregion
|
|
18
|
-
export { StructuredData_exports, derive };
|