diff --git a/CHANGELOG.md b/CHANGELOG.md index 28276b962..bddbd8725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - The page navigation sidebar no longer incorrectly includes re-exports if the same member is exported with multiple names #2625. - Page navigation now ensures the current page is visible when the page is first loaded, #2626. - If a relative linked image is referenced multiple times, TypeDoc will no longer sometimes produce invalid links to the image #2627. +- `@link` tags will now be validated in referenced markdown documents, #2629. +- `@link` tags are now resolved in project documents, #2629. - Comments on re-exports are now rendered. ### Thanks! diff --git a/internal-docs/plugins.md b/internal-docs/plugins.md index 57110eca2..3520453d1 100644 --- a/internal-docs/plugins.md +++ b/internal-docs/plugins.md @@ -2,13 +2,14 @@ title: Plugins children: - ./components-and-events.md + - ./third-party-symbols.md --- # Writing a TypeDoc Plugin TypeDoc supports plugins which can modify how projects are converted, how converted symbols are organized, and how they are displayed, among other things. Plugins are Node modules which -export a single `load` function that will be called by TypeDoc with the [Application] instance +export a single `load` function that will be called by TypeDoc with the {@link Application} instance which they are to be attached to. Plugins should assume that they may be loaded multiple times for different applications, and that a single load of an application class may be used to convert multiple projects. @@ -26,8 +27,8 @@ export function load(app) { ``` Plugins affect TypeDoc's execution by attaching event listeners to one or many events that will be -fired during conversion and rendering. Events are available on the [Application], [Converter], -[Renderer], and [Serializer]/[Deserializer] classes. There are static `EVENT_*` properties on those +fired during conversion and rendering. Events are available on the {@link Application}, {@link Converter}, +{@link Renderer}, and {@link Serializer}/{@link Deserializer} classes. There are static `EVENT_*` properties on those classes which describe the available events. The best way to learn what's available to plugins is to browse the docs, or look at the source code @@ -45,10 +46,5 @@ TypeDoc works. The [development page](https://typedoc.org/guides/development/) o If you have specific questions regarding plugin development, please open an issue or ask in the [TypeScript Discord] #typedoc channel. -[Application]: https://typedoc.org/api/classes/Application.html -[Converter]: https://typedoc.org/api/classes/Converter.html -[Renderer]: https://typedoc.org/api/classes/Renderer.html -[Serializer]: https://typedoc.org/api/classes/Serializer.html -[Deserializer]: https://typedoc.org/api/classes/Deserializer.html [typedoc-plugin-mdn-links]: https://github.com/Gerrit0/typedoc-plugin-mdn-links/blob/main/src/index.ts [TypeScript Discord]: https://discord.gg/typescript diff --git a/internal-docs/third-party-symbols.md b/internal-docs/third-party-symbols.md index bf7a1caee..eab94b910 100644 --- a/internal-docs/third-party-symbols.md +++ b/internal-docs/third-party-symbols.md @@ -1,3 +1,7 @@ +--- +title: Third Party Symbols +--- + # Third Party Symbols TypeDoc 0.22 added support for linking to third party sites by associating a symbol name with npm packages. @@ -47,7 +51,8 @@ A wildcard can be used to provide a fallback link to any unmapped type. } ``` -Plugins can add support for linking to third party sites by calling [`app.converter.addUnknownSymbolResolver`][addUnknownSymbolResolver]. +Plugins can add support for linking to third party sites by calling +{@link Converter.addUnknownSymbolResolver | `app.converter.addUnknownSymbolResolver`} If the given symbol is unknown, or does not appear in the documentation site, the resolver may return `undefined` and no link will be rendered unless provided by another resolver. @@ -150,12 +155,10 @@ export function load(app: Application) { ``` The unknown symbol resolver will also be passed the reflection containing the link -and, if the link was defined by the user, the [CommentDisplayPart] which was parsed into the [DeclarationReference] provided as the first argument. +and, if the link was defined by the user, the {@link Models.CommentDisplayPart} which was parsed into the +{@link DeclarationReference} provided as the first argument. -If `--useTsLinkResolution` is on (the default), it may also be passed a [ReflectionSymbolId] referencing the symbol that TypeScript resolves the link to. +If `--useTsLinkResolution` is on (the default), it may also be passed a {@link Models.ReflectionSymbolId} +referencing the symbol that TypeScript resolves the link to. [externalSymbolLinkMappings]: https://typedoc.org/options/comments/#externalsymbollinkmappings -[CommentDisplayPart]: https://typedoc.org/api/types/CommentDisplayPart.html -[DeclarationReference]: https://typedoc.org/api/interfaces/DeclarationReference.html -[ReflectionSymbolId]: https://typedoc.org/api/classes/Application.html -[addUnknownSymbolResolver]: https://typedoc.org/api/classes/Converter.html#addUnknownSymbolResolver diff --git a/src/index.ts b/src/index.ts index 5ddcd3e9b..9a20cec8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ export type { RendererHooks, NavigationElement, RendererEvents, + PageHeading, } from "./lib/output"; export { diff --git a/src/lib/converter/plugins/LinkResolverPlugin.ts b/src/lib/converter/plugins/LinkResolverPlugin.ts index dcc0c840a..6a114b90d 100644 --- a/src/lib/converter/plugins/LinkResolverPlugin.ts +++ b/src/lib/converter/plugins/LinkResolverPlugin.ts @@ -4,7 +4,6 @@ import { ConverterEvents } from "../converter-events"; import { Option, type ValidationOptions } from "../../utils"; import { ContainerReflection, - DeclarationReflection, makeRecursiveVisitor, type ProjectReflection, type Reflection, @@ -57,14 +56,18 @@ export class LinkResolverPlugin extends ConverterComponent { }, }), ); + + if (reflection.readme) { + reflection.readme = this.owner.resolveLinks( + reflection.readme, + reflection, + ); + } } - if ( - reflection instanceof DeclarationReflection && - reflection.readme - ) { - reflection.readme = this.owner.resolveLinks( - reflection.readme, + if (reflection.isDocument()) { + reflection.content = this.owner.resolveLinks( + reflection.content, reflection, ); } diff --git a/src/lib/internationalization/translatable.ts b/src/lib/internationalization/translatable.ts index 67fb8fd72..9a452cb93 100644 --- a/src/lib/internationalization/translatable.ts +++ b/src/lib/internationalization/translatable.ts @@ -103,6 +103,8 @@ export const translatable = { failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2: `Failed to resolve link to "{0}" in comment for {1}. You may have wanted "{2}"`, failed_to_resolve_link_to_0_in_readme_for_1: `Failed to resolve link to "{0}" in readme for {1}`, failed_to_resolve_link_to_0_in_readme_for_1_may_have_meant_2: `Failed to resolve link to "{0}" in readme for {1}. You may have wanted "{2}"`, + failed_to_resolve_link_to_0_in_document_1: `Failed to resolve link to "{0}" in document {1}`, + failed_to_resolve_link_to_0_in_document_1_may_have_meant_2: `Failed to resolve link to "{0}" in document {1}. You may have wanted "{2}"`, type_0_defined_in_1_is_referenced_by_2_but_not_included_in_docs: `{0}, defined in {1}, is referenced by {2} but not included in the documentation`, reflection_0_kind_1_defined_in_2_does_not_have_any_documentation: `{0} ({1}), defined in {2}, does not have any documentation`, invalid_intentionally_not_exported_symbols_0: diff --git a/src/lib/output/index.ts b/src/lib/output/index.ts index 9046992b3..411ab95de 100644 --- a/src/lib/output/index.ts +++ b/src/lib/output/index.ts @@ -1,4 +1,10 @@ -export { PageEvent, RendererEvent, MarkdownEvent, IndexEvent } from "./events"; +export { + PageEvent, + RendererEvent, + MarkdownEvent, + IndexEvent, + type PageHeading, +} from "./events"; export { UrlMapping } from "./models/UrlMapping"; export type { RenderTemplate } from "./models/UrlMapping"; export { Renderer, type RendererEvents } from "./renderer"; diff --git a/src/lib/validation/links.ts b/src/lib/validation/links.ts index 3a2c103d3..dd63a1fdc 100644 --- a/src/lib/validation/links.ts +++ b/src/lib/validation/links.ts @@ -1,4 +1,5 @@ import { + type Reflection, ReflectionKind, type Comment, type CommentDisplayPart, @@ -17,7 +18,7 @@ function getBrokenPartLinks(parts: readonly CommentDisplayPart[]) { linkTags.includes(part.tag) && !part.target ) { - links.push(part.text); + links.push(part.text.trim()); } } @@ -40,39 +41,47 @@ export function validateLinks( logger: Logger, ): void { for (const id in project.reflections) { - const reflection = project.reflections[id]; + checkReflection(project.reflections[id], logger); + } + + if (!(project.id in project.reflections)) { + checkReflection(project, logger); + } +} - if (reflection.isProject() || reflection.isDeclaration()) { - for (const broken of getBrokenPartLinks(reflection.readme || [])) { - // #2360, "@" is a future reserved character in TSDoc component paths - // If a link starts with it, and doesn't include a module source indicator "!" - // then the user probably is trying to link to a package containing "@" with an absolute link. - if (broken.startsWith("@") && !broken.includes("!")) { - logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_readme_for_1_may_have_meant_2( - broken, - reflection.getFriendlyFullName(), - broken.replace(/[.#~]/, "!"), - ), - ); - } else { - logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_readme_for_1( - broken, - reflection.getFriendlyFullName(), - ), - ); - } +function checkReflection(reflection: Reflection, logger: Logger) { + if (reflection.isProject() || reflection.isDeclaration()) { + for (const broken of getBrokenPartLinks(reflection.readme || [])) { + // #2360, "@" is a future reserved character in TSDoc component paths + // If a link starts with it, and doesn't include a module source indicator "!" + // then the user probably is trying to link to a package containing "@" with an absolute link. + if (broken.startsWith("@") && !broken.includes("!")) { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_readme_for_1_may_have_meant_2( + broken, + reflection.getFriendlyFullName(), + broken.replace(/[.#~]/, "!"), + ), + ); + } else { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_readme_for_1( + broken, + reflection.getFriendlyFullName(), + ), + ); } } + } - for (const broken of getBrokenLinks(reflection.comment)) { + if (reflection.isDocument()) { + for (const broken of getBrokenPartLinks(reflection.content)) { // #2360, "@" is a future reserved character in TSDoc component paths // If a link starts with it, and doesn't include a module source indicator "!" // then the user probably is trying to link to a package containing "@" with an absolute link. if (broken.startsWith("@") && !broken.includes("!")) { logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2( + logger.i18n.failed_to_resolve_link_to_0_in_document_1_may_have_meant_2( broken, reflection.getFriendlyFullName(), broken.replace(/[.#~]/, "!"), @@ -80,39 +89,61 @@ export function validateLinks( ); } else { logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1( + logger.i18n.failed_to_resolve_link_to_0_in_document_1( broken, reflection.getFriendlyFullName(), ), ); } } + } - if ( - reflection.isDeclaration() && - reflection.kindOf(ReflectionKind.TypeAlias) && - reflection.type?.type === "union" && - reflection.type.elementSummaries - ) { - for (const broken of reflection.type.elementSummaries.flatMap( - getBrokenPartLinks, - )) { - if (broken.startsWith("@") && !broken.includes("!")) { - logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2( - broken, - reflection.getFriendlyFullName(), - broken.replace(/[.#~]/, "!"), - ), - ); - } else { - logger.warn( - logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1( - broken, - reflection.getFriendlyFullName(), - ), - ); - } + for (const broken of getBrokenLinks(reflection.comment)) { + // #2360, "@" is a future reserved character in TSDoc component paths + // If a link starts with it, and doesn't include a module source indicator "!" + // then the user probably is trying to link to a package containing "@" with an absolute link. + if (broken.startsWith("@") && !broken.includes("!")) { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2( + broken, + reflection.getFriendlyFullName(), + broken.replace(/[.#~]/, "!"), + ), + ); + } else { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1( + broken, + reflection.getFriendlyFullName(), + ), + ); + } + } + + if ( + reflection.isDeclaration() && + reflection.kindOf(ReflectionKind.TypeAlias) && + reflection.type?.type === "union" && + reflection.type.elementSummaries + ) { + for (const broken of reflection.type.elementSummaries.flatMap( + getBrokenPartLinks, + )) { + if (broken.startsWith("@") && !broken.includes("!")) { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2( + broken, + reflection.getFriendlyFullName(), + broken.replace(/[.#~]/, "!"), + ), + ); + } else { + logger.warn( + logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1( + broken, + reflection.getFriendlyFullName(), + ), + ); } } }