Skip to content

Commit 3872463

Browse files
committed
Add support for external resolution on @link tags
Half of #2030 complete! externalSymbolLinkMappings tomorrow.
1 parent 02ec72b commit 3872463

File tree

17 files changed

+337
-154
lines changed

17 files changed

+337
-154
lines changed

.config/typedoc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"SORT_STRATEGIES",
99
"_ModelToObject",
1010
"EventHooksMomento",
11-
"MarkedPlugin"
11+
"MarkedPlugin",
12+
"MeaningKeywords"
1213
],
1314
"sort": ["kind", "instance-first", "required-first", "alphabetical"],
1415
"entryPoints": ["../src"],

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Unreleased
22

3+
### Features
4+
5+
- External link resolvers defined with `addUnknownSymbolResolver` will now be checked when resolving `@link` tags, #2030.
6+
Note: To support this, resolution will now happen during conversion, and as such, `Renderer.addUnknownSymbolResolver` has been
7+
soft deprecated in favor of `Converter.addUnknownSymbolResolver`. Plugins should update to use the method on `Converter`.
8+
`DefaultThemeRenderContext.attemptExternalResolution` has also been deprecated since it will repeat work done during conversion,
9+
use `ReferenceType.externalUrl` instead.
10+
- Added `Converter.addUnknownSymbolResolver` for use by plugins supporting external links.
11+
312
### Bug Fixes
413

514
- Fixed conversion of object literal types containing construct signatures, #2036.

internal-docs/third-party-symbols.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,42 @@ and no link will be rendered unless provided by another resolver.
99
The following plugin will resolve a few types from React to links on the official React documentation site.
1010

1111
```ts
12-
import { Application } from "typedoc";
12+
import { Application, type DeclarationReference } from "typedoc";
1313

1414
const knownSymbols = {
1515
Component: "https://reactjs.org/docs/react-component.html",
1616
PureComponent: "https://reactjs.org/docs/react-api.html#reactpurecomponent",
1717
};
1818

1919
export function load(app: Application) {
20-
app.renderer.addUnknownSymbolResolver("@types/react", (name: string) => {
21-
if (knownSymbols.hasOwnProperty(name)) {
22-
return knownSymbols[name as never];
20+
app.converter.addUnknownSymbolResolver((ref: DeclarationReference) => {
21+
if (
22+
ref.moduleSource !== "@types/react" &&
23+
ref.moduleSource !== "react"
24+
) {
25+
return;
26+
}
27+
28+
// If someone did {@link react!}, link them directly to the home page.
29+
if (!ref.symbolReference) {
30+
return "https://reactjs.org/";
31+
}
32+
33+
// Otherwise, we need to navigate through the symbol reference to
34+
// determine where they meant to link to. Since the symbols we know
35+
// about are all a single "level" deep, this is pretty simple.
36+
37+
if (!ref.symbolReference.path) {
38+
// Someone included a meaning, but not a path.
39+
// https://typedoc.org/guides/declaration-references/#meaning
40+
return;
41+
}
42+
43+
if (ref.symbolReference.path.length === 1) {
44+
const name = ref.symbolReference.path[0].path;
45+
if (knownSymbols.hasOwnProperty(name)) {
46+
return knownSymbols[name as never];
47+
}
2348
}
2449
});
2550
}

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ export { EventDispatcher, Event } from "./lib/utils/events";
44
export { resetReflectionID } from "./lib/models/reflections/abstract";
55
export { normalizePath } from "./lib/utils/fs";
66
export * from "./lib/models";
7-
export { Converter, Context, type CommentParserConfig } from "./lib/converter";
7+
export {
8+
Converter,
9+
Context,
10+
type CommentParserConfig,
11+
type DeclarationReference,
12+
type SymbolReference,
13+
type ComponentPath,
14+
type Meaning,
15+
type MeaningKeyword,
16+
} from "./lib/converter";
817

918
export {
1019
Renderer,

src/lib/converter/comments/linkResolver.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
Reflection,
88
} from "../../models";
99
import type { Logger, ValidationOptions } from "../../utils";
10-
import { parseDeclarationReference } from "./declarationReference";
10+
import {
11+
DeclarationReference,
12+
parseDeclarationReference,
13+
} from "./declarationReference";
1114
import { resolveDeclarationReference } from "./declarationReferenceResolver";
1215

1316
const urlPrefix = /^(http|ftp)s?:\/\//;
@@ -17,7 +20,8 @@ export function resolveLinks(
1720
comment: Comment,
1821
reflection: Reflection,
1922
validation: ValidationOptions,
20-
logger: Logger
23+
logger: Logger,
24+
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
2125
) {
2226
let warned = false;
2327
const warn = () => {
@@ -34,15 +38,17 @@ export function resolveLinks(
3438
comment.summary,
3539
warn,
3640
validation,
37-
logger
41+
logger,
42+
attemptExternalResolve
3843
);
3944
for (const tag of comment.blockTags) {
4045
tag.content = resolvePartLinks(
4146
reflection,
4247
tag.content,
4348
warn,
4449
validation,
45-
logger
50+
logger,
51+
attemptExternalResolve
4652
);
4753
}
4854

@@ -52,7 +58,8 @@ export function resolveLinks(
5258
reflection.readme,
5359
warn,
5460
validation,
55-
logger
61+
logger,
62+
attemptExternalResolve
5663
);
5764
}
5865
}
@@ -62,10 +69,18 @@ export function resolvePartLinks(
6269
parts: readonly CommentDisplayPart[],
6370
warn: () => void,
6471
validation: ValidationOptions,
65-
logger: Logger
72+
logger: Logger,
73+
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
6674
): CommentDisplayPart[] {
6775
return parts.flatMap((part) =>
68-
processPart(reflection, part, warn, validation, logger)
76+
processPart(
77+
reflection,
78+
part,
79+
warn,
80+
validation,
81+
logger,
82+
attemptExternalResolve
83+
)
6984
);
7085
}
7186

@@ -74,7 +89,8 @@ function processPart(
7489
part: CommentDisplayPart,
7590
warn: () => void,
7691
validation: ValidationOptions,
77-
logger: Logger
92+
logger: Logger,
93+
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
7894
): CommentDisplayPart | CommentDisplayPart[] {
7995
if (part.kind === "text" && brackets.test(part.text)) {
8096
warn();
@@ -87,11 +103,16 @@ function processPart(
87103
part.tag === "@linkcode" ||
88104
part.tag === "@linkplain"
89105
) {
90-
return resolveLinkTag(reflection, part, (msg: string) => {
91-
if (validation.invalidLink) {
92-
logger.warn(msg);
106+
return resolveLinkTag(
107+
reflection,
108+
part,
109+
attemptExternalResolve,
110+
(msg: string) => {
111+
if (validation.invalidLink) {
112+
logger.warn(msg);
113+
}
93114
}
94-
});
115+
);
95116
}
96117
}
97118

@@ -101,6 +122,7 @@ function processPart(
101122
function resolveLinkTag(
102123
reflection: Reflection,
103124
part: InlineTagDisplayPart,
125+
attemptExternalResolve: (ref: DeclarationReference) => string | undefined,
104126
warn: (message: string) => void
105127
) {
106128
let pos = 0;
@@ -118,6 +140,11 @@ function resolveLinkTag(
118140
// Got one, great! Try to resolve the link
119141
target = resolveDeclarationReference(reflection, declRef[0]);
120142
pos = declRef[1];
143+
144+
// If we didn't find a link, it might be a @link tag to an external symbol, check that next.
145+
if (!target) {
146+
target = attemptExternalResolve(declRef[0]);
147+
}
121148
}
122149

123150
if (!target) {
@@ -133,7 +160,7 @@ function resolveLinkTag(
133160
// method... this should go away in 0.24, once people have had a chance to migrate any failing links.
134161
if (!target) {
135162
const resolved = legacyResolveLinkTag(reflection, part);
136-
if (resolved) {
163+
if (resolved.target) {
137164
warn(
138165
`Failed to resolve {@link ${origText}} in ${reflection.getFriendlyFullName()} with declaration references. This link will break in v0.24.`
139166
);

src/lib/converter/converter.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
import { parseComment } from "./comments/parser";
2929
import { lexCommentString } from "./comments/rawLexer";
3030
import { resolvePartLinks, resolveLinks } from "./comments/linkResolver";
31+
import type { DeclarationReference } from "./comments/declarationReference";
3132

3233
/**
3334
* Compiles source files using TypeScript and converts compiler symbols to reflections.
@@ -41,29 +42,29 @@ export class Converter extends ChildableComponent<
4142
Application,
4243
ConverterComponent
4344
> {
44-
/**
45-
* The human readable name of the project. Used within the templates to set the title of the document.
46-
*/
47-
@BindOption("name")
48-
name!: string;
49-
45+
/** @internal */
5046
@BindOption("externalPattern")
5147
externalPattern!: string[];
5248
private externalPatternCache?: Minimatch[];
5349
private excludeCache?: Minimatch[];
5450

51+
/** @internal */
5552
@BindOption("excludeExternals")
5653
excludeExternals!: boolean;
5754

55+
/** @internal */
5856
@BindOption("excludeNotDocumented")
5957
excludeNotDocumented!: boolean;
6058

59+
/** @internal */
6160
@BindOption("excludePrivate")
6261
excludePrivate!: boolean;
6362

63+
/** @internal */
6464
@BindOption("excludeProtected")
6565
excludeProtected!: boolean;
6666

67+
/** @internal */
6768
@BindOption("commentStyle")
6869
commentStyle!: CommentStyle;
6970

@@ -72,6 +73,9 @@ export class Converter extends ChildableComponent<
7273
validation!: ValidationOptions;
7374

7475
private _config?: CommentParserConfig;
76+
private _externalSymbolResolvers: Array<
77+
(ref: DeclarationReference) => string | undefined
78+
> = [];
7579

7680
get config(): CommentParserConfig {
7781
return this._config || this._buildCommentParserConfig();
@@ -164,7 +168,9 @@ export class Converter extends ChildableComponent<
164168
const programs = entryPoints.map((e) => e.program);
165169
this.externalPatternCache = void 0;
166170

167-
const project = new ProjectReflection(this.name);
171+
const project = new ProjectReflection(
172+
this.application.options.getValue("name")
173+
);
168174
const context = new Context(this, programs, project);
169175

170176
this.trigger(Converter.EVENT_BEGIN, context);
@@ -211,6 +217,32 @@ export class Converter extends ChildableComponent<
211217
);
212218
}
213219

220+
/**
221+
* Adds a new resolver that the theme can use to try to figure out how to link to a symbol declared
222+
* by a third-party library which is not included in the documentation.
223+
*
224+
* The resolver function will be passed a declaration reference which it can attempt to resolve. If
225+
* resolution fails, the function should return undefined.
226+
*
227+
* Note: This will be used for both references to types declared in node_modules (in which case the
228+
* reference passed will have the `moduleSource` set and the `symbolReference` will navigate via `.`)
229+
* and user defined \{\@link\} tags which cannot be resolved.
230+
* @since 0.22.14
231+
*/
232+
addUnknownSymbolResolver(
233+
resolver: (ref: DeclarationReference) => string | undefined
234+
): void {
235+
this._externalSymbolResolvers.push(resolver);
236+
}
237+
238+
/** @internal */
239+
resolveExternalLink(ref: DeclarationReference): string | undefined {
240+
for (const resolver of this._externalSymbolResolvers) {
241+
const resolved = resolver(ref);
242+
if (resolved) return resolved;
243+
}
244+
}
245+
214246
resolveLinks(comment: Comment, owner: Reflection): void;
215247
resolveLinks(
216248
parts: readonly CommentDisplayPart[],
@@ -221,7 +253,13 @@ export class Converter extends ChildableComponent<
221253
owner: Reflection
222254
): CommentDisplayPart[] | undefined {
223255
if (comment instanceof Comment) {
224-
resolveLinks(comment, owner, this.validation, this.owner.logger);
256+
resolveLinks(
257+
comment,
258+
owner,
259+
this.validation,
260+
this.owner.logger,
261+
(ref) => this.resolveExternalLink(ref)
262+
);
225263
} else {
226264
let warned = false;
227265
const warn = () => {
@@ -238,7 +276,8 @@ export class Converter extends ChildableComponent<
238276
comment,
239277
warn,
240278
this.validation,
241-
this.owner.logger
279+
this.owner.logger,
280+
(ref) => this.resolveExternalLink(ref)
242281
);
243282
}
244283
}

src/lib/converter/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@ export { Context } from "./context";
22
export { Converter } from "./converter";
33
export type { CommentParserConfig } from "./comments/index";
44
export { convertDefaultValue, convertExpression } from "./convert-expression";
5+
export type {
6+
DeclarationReference,
7+
SymbolReference,
8+
ComponentPath,
9+
Meaning,
10+
MeaningKeyword,
11+
} from "./comments/declarationReference";
512

613
import "./plugins/index";

src/lib/converter/plugins/LinkResolverPlugin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Context } from "../../converter";
33
import { ConverterEvents } from "../converter-events";
44
import { BindOption, ValidationOptions } from "../../utils";
55
import { DeclarationReflection } from "../../models";
6+
import { discoverAllReferenceTypes } from "../../utils/reflections";
67

78
/**
89
* A plugin that resolves `{@link Foo}` tags.
@@ -43,5 +44,16 @@ export class LinkResolverPlugin extends ConverterComponent {
4344
context.project
4445
);
4546
}
47+
48+
for (const { type } of discoverAllReferenceTypes(
49+
context.project,
50+
false
51+
)) {
52+
if (!type.reflection) {
53+
type.externalUrl = context.converter.resolveExternalLink(
54+
type.toDeclarationReference()
55+
);
56+
}
57+
}
4658
}
4759
}

0 commit comments

Comments
 (0)