Skip to content

Commit

Permalink
Add support for external resolution on @link tags
Browse files Browse the repository at this point in the history
Half of #2030 complete! externalSymbolLinkMappings tomorrow.
  • Loading branch information
Gerrit0 committed Sep 1, 2022
1 parent 02ec72b commit 3872463
Show file tree
Hide file tree
Showing 17 changed files with 337 additions and 154 deletions.
3 changes: 2 additions & 1 deletion .config/typedoc.json
Expand Up @@ -8,7 +8,8 @@
"SORT_STRATEGIES",
"_ModelToObject",
"EventHooksMomento",
"MarkedPlugin"
"MarkedPlugin",
"MeaningKeywords"
],
"sort": ["kind", "instance-first", "required-first", "alphabetical"],
"entryPoints": ["../src"],
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,14 @@
# Unreleased

### Features

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

### Bug Fixes

- Fixed conversion of object literal types containing construct signatures, #2036.
Expand Down
33 changes: 29 additions & 4 deletions internal-docs/third-party-symbols.md
Expand Up @@ -9,17 +9,42 @@ and no link will be rendered unless provided by another resolver.
The following plugin will resolve a few types from React to links on the official React documentation site.

```ts
import { Application } from "typedoc";
import { Application, type DeclarationReference } from "typedoc";

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

export function load(app: Application) {
app.renderer.addUnknownSymbolResolver("@types/react", (name: string) => {
if (knownSymbols.hasOwnProperty(name)) {
return knownSymbols[name as never];
app.converter.addUnknownSymbolResolver((ref: DeclarationReference) => {
if (
ref.moduleSource !== "@types/react" &&
ref.moduleSource !== "react"
) {
return;
}

// If someone did {@link react!}, link them directly to the home page.
if (!ref.symbolReference) {
return "https://reactjs.org/";
}

// Otherwise, we need to navigate through the symbol reference to
// determine where they meant to link to. Since the symbols we know
// about are all a single "level" deep, this is pretty simple.

if (!ref.symbolReference.path) {
// Someone included a meaning, but not a path.
// https://typedoc.org/guides/declaration-references/#meaning
return;
}

if (ref.symbolReference.path.length === 1) {
const name = ref.symbolReference.path[0].path;
if (knownSymbols.hasOwnProperty(name)) {
return knownSymbols[name as never];
}
}
});
}
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Expand Up @@ -4,7 +4,16 @@ export { EventDispatcher, Event } from "./lib/utils/events";
export { resetReflectionID } from "./lib/models/reflections/abstract";
export { normalizePath } from "./lib/utils/fs";
export * from "./lib/models";
export { Converter, Context, type CommentParserConfig } from "./lib/converter";
export {
Converter,
Context,
type CommentParserConfig,
type DeclarationReference,
type SymbolReference,
type ComponentPath,
type Meaning,
type MeaningKeyword,
} from "./lib/converter";

export {
Renderer,
Expand Down
53 changes: 40 additions & 13 deletions src/lib/converter/comments/linkResolver.ts
Expand Up @@ -7,7 +7,10 @@ import {
Reflection,
} from "../../models";
import type { Logger, ValidationOptions } from "../../utils";
import { parseDeclarationReference } from "./declarationReference";
import {
DeclarationReference,
parseDeclarationReference,
} from "./declarationReference";
import { resolveDeclarationReference } from "./declarationReferenceResolver";

const urlPrefix = /^(http|ftp)s?:\/\//;
Expand All @@ -17,7 +20,8 @@ export function resolveLinks(
comment: Comment,
reflection: Reflection,
validation: ValidationOptions,
logger: Logger
logger: Logger,
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
) {
let warned = false;
const warn = () => {
Expand All @@ -34,15 +38,17 @@ export function resolveLinks(
comment.summary,
warn,
validation,
logger
logger,
attemptExternalResolve
);
for (const tag of comment.blockTags) {
tag.content = resolvePartLinks(
reflection,
tag.content,
warn,
validation,
logger
logger,
attemptExternalResolve
);
}

Expand All @@ -52,7 +58,8 @@ export function resolveLinks(
reflection.readme,
warn,
validation,
logger
logger,
attemptExternalResolve
);
}
}
Expand All @@ -62,10 +69,18 @@ export function resolvePartLinks(
parts: readonly CommentDisplayPart[],
warn: () => void,
validation: ValidationOptions,
logger: Logger
logger: Logger,
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
): CommentDisplayPart[] {
return parts.flatMap((part) =>
processPart(reflection, part, warn, validation, logger)
processPart(
reflection,
part,
warn,
validation,
logger,
attemptExternalResolve
)
);
}

Expand All @@ -74,7 +89,8 @@ function processPart(
part: CommentDisplayPart,
warn: () => void,
validation: ValidationOptions,
logger: Logger
logger: Logger,
attemptExternalResolve: (ref: DeclarationReference) => string | undefined
): CommentDisplayPart | CommentDisplayPart[] {
if (part.kind === "text" && brackets.test(part.text)) {
warn();
Expand All @@ -87,11 +103,16 @@ function processPart(
part.tag === "@linkcode" ||
part.tag === "@linkplain"
) {
return resolveLinkTag(reflection, part, (msg: string) => {
if (validation.invalidLink) {
logger.warn(msg);
return resolveLinkTag(
reflection,
part,
attemptExternalResolve,
(msg: string) => {
if (validation.invalidLink) {
logger.warn(msg);
}
}
});
);
}
}

Expand All @@ -101,6 +122,7 @@ function processPart(
function resolveLinkTag(
reflection: Reflection,
part: InlineTagDisplayPart,
attemptExternalResolve: (ref: DeclarationReference) => string | undefined,
warn: (message: string) => void
) {
let pos = 0;
Expand All @@ -118,6 +140,11 @@ function resolveLinkTag(
// Got one, great! Try to resolve the link
target = resolveDeclarationReference(reflection, declRef[0]);
pos = declRef[1];

// If we didn't find a link, it might be a @link tag to an external symbol, check that next.
if (!target) {
target = attemptExternalResolve(declRef[0]);
}
}

if (!target) {
Expand All @@ -133,7 +160,7 @@ function resolveLinkTag(
// method... this should go away in 0.24, once people have had a chance to migrate any failing links.
if (!target) {
const resolved = legacyResolveLinkTag(reflection, part);
if (resolved) {
if (resolved.target) {
warn(
`Failed to resolve {@link ${origText}} in ${reflection.getFriendlyFullName()} with declaration references. This link will break in v0.24.`
);
Expand Down
57 changes: 48 additions & 9 deletions src/lib/converter/converter.ts
Expand Up @@ -28,6 +28,7 @@ import type {
import { parseComment } from "./comments/parser";
import { lexCommentString } from "./comments/rawLexer";
import { resolvePartLinks, resolveLinks } from "./comments/linkResolver";
import type { DeclarationReference } from "./comments/declarationReference";

/**
* Compiles source files using TypeScript and converts compiler symbols to reflections.
Expand All @@ -41,29 +42,29 @@ export class Converter extends ChildableComponent<
Application,
ConverterComponent
> {
/**
* The human readable name of the project. Used within the templates to set the title of the document.
*/
@BindOption("name")
name!: string;

/** @internal */
@BindOption("externalPattern")
externalPattern!: string[];
private externalPatternCache?: Minimatch[];
private excludeCache?: Minimatch[];

/** @internal */
@BindOption("excludeExternals")
excludeExternals!: boolean;

/** @internal */
@BindOption("excludeNotDocumented")
excludeNotDocumented!: boolean;

/** @internal */
@BindOption("excludePrivate")
excludePrivate!: boolean;

/** @internal */
@BindOption("excludeProtected")
excludeProtected!: boolean;

/** @internal */
@BindOption("commentStyle")
commentStyle!: CommentStyle;

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

private _config?: CommentParserConfig;
private _externalSymbolResolvers: Array<
(ref: DeclarationReference) => string | undefined
> = [];

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

const project = new ProjectReflection(this.name);
const project = new ProjectReflection(
this.application.options.getValue("name")
);
const context = new Context(this, programs, project);

this.trigger(Converter.EVENT_BEGIN, context);
Expand Down Expand Up @@ -211,6 +217,32 @@ export class Converter extends ChildableComponent<
);
}

/**
* Adds a new resolver that the theme can use to try to figure out how to link to a symbol declared
* by a third-party library which is not included in the documentation.
*
* The resolver function will be passed a declaration reference which it can attempt to resolve. If
* resolution fails, the function should return undefined.
*
* Note: This will be used for both references to types declared in node_modules (in which case the
* reference passed will have the `moduleSource` set and the `symbolReference` will navigate via `.`)
* and user defined \{\@link\} tags which cannot be resolved.
* @since 0.22.14
*/
addUnknownSymbolResolver(
resolver: (ref: DeclarationReference) => string | undefined
): void {
this._externalSymbolResolvers.push(resolver);
}

/** @internal */
resolveExternalLink(ref: DeclarationReference): string | undefined {
for (const resolver of this._externalSymbolResolvers) {
const resolved = resolver(ref);
if (resolved) return resolved;
}
}

resolveLinks(comment: Comment, owner: Reflection): void;
resolveLinks(
parts: readonly CommentDisplayPart[],
Expand All @@ -221,7 +253,13 @@ export class Converter extends ChildableComponent<
owner: Reflection
): CommentDisplayPart[] | undefined {
if (comment instanceof Comment) {
resolveLinks(comment, owner, this.validation, this.owner.logger);
resolveLinks(
comment,
owner,
this.validation,
this.owner.logger,
(ref) => this.resolveExternalLink(ref)
);
} else {
let warned = false;
const warn = () => {
Expand All @@ -238,7 +276,8 @@ export class Converter extends ChildableComponent<
comment,
warn,
this.validation,
this.owner.logger
this.owner.logger,
(ref) => this.resolveExternalLink(ref)
);
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/converter/index.ts
Expand Up @@ -2,5 +2,12 @@ export { Context } from "./context";
export { Converter } from "./converter";
export type { CommentParserConfig } from "./comments/index";
export { convertDefaultValue, convertExpression } from "./convert-expression";
export type {
DeclarationReference,
SymbolReference,
ComponentPath,
Meaning,
MeaningKeyword,
} from "./comments/declarationReference";

import "./plugins/index";
12 changes: 12 additions & 0 deletions src/lib/converter/plugins/LinkResolverPlugin.ts
Expand Up @@ -3,6 +3,7 @@ import type { Context } from "../../converter";
import { ConverterEvents } from "../converter-events";
import { BindOption, ValidationOptions } from "../../utils";
import { DeclarationReflection } from "../../models";
import { discoverAllReferenceTypes } from "../../utils/reflections";

/**
* A plugin that resolves `{@link Foo}` tags.
Expand Down Expand Up @@ -43,5 +44,16 @@ export class LinkResolverPlugin extends ConverterComponent {
context.project
);
}

for (const { type } of discoverAllReferenceTypes(
context.project,
false
)) {
if (!type.reflection) {
type.externalUrl = context.converter.resolveExternalLink(
type.toDeclarationReference()
);
}
}
}
}

0 comments on commit 3872463

Please sign in to comment.