Skip to content

Commit 9e7748f

Browse files
committed
fix(docs-infra): add target="_blank" to external links in @see JSDoc tags
External links rendered via `@see {@link ...}` and `@see [label](url)` were not opening in a new tab. The `getHtmlAdditionalLinks` function now sets `target: '_blank'` on `LinkEntryRenderable` objects whose URL is external, and `docs-pill-row.tsx` passes the `target` attribute to the rendered anchor tag. Fixes: angular#69593
1 parent d109cc5 commit 9e7748f

4 files changed

Lines changed: 23 additions & 6 deletions

File tree

‎adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.mts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export interface LinkEntryRenderable {
149149
label: string;
150150
url: string;
151151
title?: string;
152+
target?: string;
152153
}
153154

154155
export type CliOptionRenderable = CliOption & {

‎adev/shared-docs/pipeline/api-gen/rendering/templates/docs-pill-row.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function DocsPillRow(props: {links: LinkEntryRenderable[]}) {
2020
class="docs-pill"
2121
href={link.url}
2222
title={link.title}
23+
target={link.target}
2324
dangerouslySetInnerHTML={{__html: link.label}}
2425
></a>
2526
))}

‎adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,58 +89,70 @@ describe('jsdoc transforms', () => {
8989
label: 'Angular',
9090
url: 'https://angular.io',
9191
title: undefined,
92+
target: '_blank',
9293
});
9394

9495
expect(entry.additionalLinks[1]).toEqual({
9596
label: 'Angular',
9697
url: 'https://angular.io',
9798
title: 'Angular',
99+
target: '_blank',
98100
});
99101

100102
expect(entry.additionalLinks[2]).toEqual({
101103
label: 'Route',
102104
url: '/api/test/Route',
105+
target: undefined,
103106
});
104107

105108
expect(entry.additionalLinks[3]).toEqual({
106109
label: 'Something else',
107110
url: '/api/test/Route',
111+
target: undefined,
108112
});
109113

110114
expect(entry.additionalLinks[4]).toEqual({
111115
label: 'someMethod',
112116
url: '/api/test/Router#someMethod',
117+
target: undefined,
113118
});
114119
expect(entry.additionalLinks[5]).toEqual({
115120
label: 'someMethodWithParenthesis()',
116121
url: '/api/test/Router#someMethodWithParenthesis',
122+
target: undefined,
117123
});
118124
expect(entry.additionalLinks[6]).toEqual({
119125
label: 'someMethod()',
120126
url: '/api/test/Router#someMethod',
127+
target: undefined,
121128
});
122129
expect(entry.additionalLinks[7]).toEqual({
123130
label: 'FormGroup.someMethod()',
124131
url: '/api/test/FormGroup#someMethod',
132+
target: undefined,
125133
});
126134

127135
expect(entry.additionalLinks[8]).toEqual({
128136
label: 'ApplicationRef',
129137
url: 'https://angular.dev/api/core/ApplicationRef',
138+
target: '_blank',
130139
});
131140
expect(entry.additionalLinks[9]).toEqual({
132141
label: 'angular.dev',
133142
url: 'https://angular.dev',
143+
target: '_blank',
134144
});
135145

136146
expect(entry.additionalLinks[10]).toEqual({
137147
label: 'ng build',
138148
url: '/cli/build',
149+
target: undefined,
139150
});
140151

141152
expect(entry.additionalLinks[11]).toEqual({
142153
label: 'Output Interop',
143154
url: '/ecosystem/rxjs-interop/output-interop',
155+
target: undefined,
144156
});
145157
});
146158

@@ -176,12 +188,14 @@ describe('jsdoc transforms', () => {
176188
'Popup attached to <code>document.body</code> with <code>createComponent</code> + <code>hostElement</code>',
177189
url: 'guide/components/programmatic-rendering#popup-attached-to-documentbody-with-createcomponent--hostelement',
178190
title: undefined,
191+
target: undefined,
179192
});
180193

181194
expect(entry.additionalLinks[2]).toEqual({
182195
label: 'Method with <code>backticks</code> in title',
183196
url: 'https://example.com',
184197
title: 'Title with `code`',
198+
target: '_blank',
185199
});
186200
});
187201

‎adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
unknownSymbolMessage,
3535
} from '../symbol-context.mjs';
3636
import {addApiLinksToHtml} from './code-transforms.mjs';
37+
import {isExternalLink} from '../../../shared/marked/helpers.mjs';
3738

3839
const JS_DOC_USAGE_NOTE_TAGS: Set<string> = new Set(['remarks', 'usageNotes', 'example']);
3940
export const JS_DOC_SEE_TAG = 'see';
@@ -144,17 +145,19 @@ function getHtmlAdditionalLinks<T extends HasJsDocTags>(entry: T): LinkEntryRend
144145
const seeAlsoLinks = entry.jsdocTags
145146
.filter((tag) => tag.name === JS_DOC_SEE_TAG)
146147
.map((tag) => tag.comment)
147-
.map((comment) => {
148+
.map((comment): LinkEntryRenderable | undefined => {
148149
// TODO: Throw when the comment is an absolute link.
149150
// With TS 5.9 this is not possible as the ts api that extracts comments from tags strips the "http" part of links.
150151

151152
const markdownLinkMatch = comment.match(markdownLinkRule);
152153

153154
if (markdownLinkMatch) {
155+
const url = markdownLinkMatch[2];
154156
return {
155157
label: convertBackticksToCodeTags(markdownLinkMatch[1]),
156-
url: markdownLinkMatch[2],
158+
url,
157159
title: markdownLinkMatch[3],
160+
target: isExternalLink(url) ? '_blank' : undefined,
158161
};
159162
}
160163

@@ -163,11 +166,9 @@ function getHtmlAdditionalLinks<T extends HasJsDocTags>(entry: T): LinkEntryRend
163166
if (linkMatch) {
164167
const link = linkMatch[1];
165168
const parsed = parseAtLink(link);
166-
if (!parsed) {
167-
return undefined;
168-
}
169+
if (!parsed) return undefined;
169170
const {url, label} = parsed;
170-
return {label, url};
171+
return {label, url, target: isExternalLink(url) ? '_blank' : undefined};
171172
}
172173

173174
return undefined;

0 commit comments

Comments
 (0)