Skip to content

Commit 3830f96

Browse files
committed
Expose Converter.resolveLinks
Resolves #2004.
1 parent 5d9a51d commit 3830f96

File tree

4 files changed

+320
-265
lines changed

4 files changed

+320
-265
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Improved schema generation to give better autocomplete for the `sort` option.
88
- Optional properties are now visually distinguished in the index/sidebar by rendering `prop` as `prop?`, #2023.
99
- `DefaultThemeRenderContext.markdown` now also accepts a `CommentDisplayPart[]` for rendering, #2004.
10+
- Expose `Converter.resolveLinks` method for use with `Converter.parseRawComment`, #2004.
1011

1112
### Bug Fixes
1213

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import * as ts from "typescript";
2+
import {
3+
Comment,
4+
CommentDisplayPart,
5+
DeclarationReflection,
6+
InlineTagDisplayPart,
7+
Reflection,
8+
} from "../../models";
9+
import type { Logger, ValidationOptions } from "../../utils";
10+
import { parseDeclarationReference } from "./declarationReference";
11+
import { resolveDeclarationReference } from "./declarationReferenceResolver";
12+
13+
const urlPrefix = /^(http|ftp)s?:\/\//;
14+
const brackets = /\[\[(?!include:)([^\]]+)\]\]/g;
15+
16+
export function resolveLinks(
17+
comment: Comment,
18+
reflection: Reflection,
19+
validation: ValidationOptions,
20+
logger: Logger
21+
) {
22+
let warned = false;
23+
const warn = () => {
24+
if (!warned) {
25+
warned = true;
26+
logger.warn(
27+
`${reflection.getFriendlyFullName()}: Comment [[target]] style links are deprecated and will be removed in 0.24`
28+
);
29+
}
30+
};
31+
32+
comment.summary = resolvePartLinks(
33+
reflection,
34+
comment.summary,
35+
warn,
36+
validation,
37+
logger
38+
);
39+
for (const tag of comment.blockTags) {
40+
tag.content = resolvePartLinks(
41+
reflection,
42+
tag.content,
43+
warn,
44+
validation,
45+
logger
46+
);
47+
}
48+
49+
if (reflection instanceof DeclarationReflection && reflection.readme) {
50+
reflection.readme = resolvePartLinks(
51+
reflection,
52+
reflection.readme,
53+
warn,
54+
validation,
55+
logger
56+
);
57+
}
58+
}
59+
60+
export function resolvePartLinks(
61+
reflection: Reflection,
62+
parts: readonly CommentDisplayPart[],
63+
warn: () => void,
64+
validation: ValidationOptions,
65+
logger: Logger
66+
): CommentDisplayPart[] {
67+
return parts.flatMap((part) =>
68+
processPart(reflection, part, warn, validation, logger)
69+
);
70+
}
71+
72+
function processPart(
73+
reflection: Reflection,
74+
part: CommentDisplayPart,
75+
warn: () => void,
76+
validation: ValidationOptions,
77+
logger: Logger
78+
): CommentDisplayPart | CommentDisplayPart[] {
79+
if (part.kind === "text" && brackets.test(part.text)) {
80+
warn();
81+
return replaceBrackets(reflection, part.text, validation, logger);
82+
}
83+
84+
if (part.kind === "inline-tag") {
85+
if (
86+
part.tag === "@link" ||
87+
part.tag === "@linkcode" ||
88+
part.tag === "@linkplain"
89+
) {
90+
return resolveLinkTag(reflection, part, (msg: string) => {
91+
if (validation.invalidLink) {
92+
logger.warn(msg);
93+
}
94+
});
95+
}
96+
}
97+
98+
return part;
99+
}
100+
101+
function resolveLinkTag(
102+
reflection: Reflection,
103+
part: InlineTagDisplayPart,
104+
warn: (message: string) => void
105+
) {
106+
let pos = 0;
107+
const end = part.text.length;
108+
while (pos < end && ts.isWhiteSpaceLike(part.text.charCodeAt(pos))) {
109+
pos++;
110+
}
111+
const origText = part.text;
112+
113+
// Try to parse one
114+
const declRef = parseDeclarationReference(part.text, pos, end);
115+
116+
let target: Reflection | string | undefined;
117+
if (declRef) {
118+
// Got one, great! Try to resolve the link
119+
target = resolveDeclarationReference(reflection, declRef[0]);
120+
pos = declRef[1];
121+
}
122+
123+
if (!target) {
124+
if (urlPrefix.test(part.text)) {
125+
const wsIndex = part.text.search(/\s/);
126+
target =
127+
wsIndex === -1 ? part.text : part.text.substring(0, wsIndex);
128+
pos = target.length;
129+
}
130+
}
131+
132+
// If resolution via a declaration reference failed, revert to the legacy "split and check"
133+
// method... this should go away in 0.24, once people have had a chance to migrate any failing links.
134+
if (!target) {
135+
const resolved = legacyResolveLinkTag(reflection, part);
136+
if (resolved) {
137+
warn(
138+
`Failed to resolve {@link ${origText}} in ${reflection.getFriendlyFullName()} with declaration references. This link will break in v0.24.`
139+
);
140+
}
141+
return resolved;
142+
}
143+
144+
// Remaining text after an optional pipe is the link text, so advance
145+
// until that's consumed.
146+
while (pos < end && ts.isWhiteSpaceLike(part.text.charCodeAt(pos))) {
147+
pos++;
148+
}
149+
if (pos < end && part.text[pos] === "|") {
150+
pos++;
151+
}
152+
153+
part.target = target;
154+
part.text =
155+
part.text.substring(pos).trim() ||
156+
(typeof target === "string" ? target : target.name);
157+
158+
return part;
159+
}
160+
161+
function legacyResolveLinkTag(
162+
reflection: Reflection,
163+
part: InlineTagDisplayPart
164+
) {
165+
const { caption, target } = splitLinkText(part.text);
166+
167+
if (urlPrefix.test(target)) {
168+
part.text = caption;
169+
part.target = target;
170+
} else {
171+
const targetRefl = reflection.findReflectionByName(target);
172+
if (targetRefl) {
173+
part.text = caption;
174+
part.target = targetRefl;
175+
}
176+
}
177+
178+
return part;
179+
}
180+
181+
function replaceBrackets(
182+
reflection: Reflection,
183+
text: string,
184+
validation: ValidationOptions,
185+
logger: Logger
186+
): CommentDisplayPart[] {
187+
const parts: CommentDisplayPart[] = [];
188+
189+
let begin = 0;
190+
brackets.lastIndex = 0;
191+
for (const match of text.matchAll(brackets)) {
192+
if (begin != match.index) {
193+
parts.push({
194+
kind: "text",
195+
text: text.substring(begin, match.index),
196+
});
197+
}
198+
begin = match.index! + match[0].length;
199+
const content = match[1];
200+
201+
const { target, caption } = splitLinkText(content);
202+
203+
if (urlPrefix.test(target)) {
204+
parts.push({
205+
kind: "inline-tag",
206+
tag: "@link",
207+
text: caption,
208+
target,
209+
});
210+
} else {
211+
const targetRefl = reflection.findReflectionByName(target);
212+
if (targetRefl) {
213+
parts.push({
214+
kind: "inline-tag",
215+
tag: "@link",
216+
text: caption,
217+
target: targetRefl,
218+
});
219+
} else {
220+
if (validation.invalidLink) {
221+
logger.warn("Failed to find target: " + content);
222+
}
223+
parts.push({
224+
kind: "inline-tag",
225+
tag: "@link",
226+
text: content,
227+
});
228+
}
229+
}
230+
}
231+
parts.push({
232+
kind: "text",
233+
text: text.substring(begin),
234+
});
235+
236+
return parts;
237+
}
238+
239+
/**
240+
* Split the given link into text and target at first pipe or space.
241+
*
242+
* @param text The source string that should be checked for a split character.
243+
* @returns An object containing the link text and target.
244+
*/
245+
function splitLinkText(text: string): { caption: string; target: string } {
246+
let splitIndex = text.indexOf("|");
247+
if (splitIndex === -1) {
248+
splitIndex = text.search(/\s/);
249+
}
250+
251+
if (splitIndex !== -1) {
252+
return {
253+
caption: text
254+
.substring(splitIndex + 1)
255+
.replace(/\n+/, " ")
256+
.trim(),
257+
target: text.substring(0, splitIndex).trim(),
258+
};
259+
} else {
260+
return {
261+
caption: text,
262+
target: text,
263+
};
264+
}
265+
}

src/lib/converter/converter.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import * as ts from "typescript";
22

33
import type { Application } from "../application";
4-
import { ProjectReflection, ReflectionKind, SomeType } from "../models/index";
4+
import {
5+
Comment,
6+
CommentDisplayPart,
7+
ProjectReflection,
8+
Reflection,
9+
ReflectionKind,
10+
SomeType,
11+
} from "../models/index";
512
import { Context } from "./context";
613
import { ConverterComponent } from "./components";
714
import { Component, ChildableComponent } from "../utils/component";
@@ -14,9 +21,13 @@ import type { IMinimatch } from "minimatch";
1421
import { hasAllFlags, hasAnyFlag } from "../utils/enum";
1522
import type { DocumentationEntryPoint } from "../utils/entry-point";
1623
import { CommentParserConfig, getComment } from "./comments";
17-
import type { CommentStyle } from "../utils/options/declaration";
24+
import type {
25+
CommentStyle,
26+
ValidationOptions,
27+
} from "../utils/options/declaration";
1828
import { parseComment } from "./comments/parser";
1929
import { lexCommentString } from "./comments/rawLexer";
30+
import { resolvePartLinks, resolveLinks } from "./comments/linkResolver";
2031

2132
/**
2233
* Compiles source files using TypeScript and converts compiler symbols to reflections.
@@ -56,6 +67,10 @@ export class Converter extends ChildableComponent<
5667
@BindOption("commentStyle")
5768
commentStyle!: CommentStyle;
5869

70+
/** @internal */
71+
@BindOption("validation")
72+
validation!: ValidationOptions;
73+
5974
private _config?: CommentParserConfig;
6075

6176
get config(): CommentParserConfig {
@@ -196,6 +211,38 @@ export class Converter extends ChildableComponent<
196211
);
197212
}
198213

214+
resolveLinks(comment: Comment, owner: Reflection): void;
215+
resolveLinks(
216+
parts: readonly CommentDisplayPart[],
217+
owner: Reflection
218+
): CommentDisplayPart[];
219+
resolveLinks(
220+
comment: Comment | readonly CommentDisplayPart[],
221+
owner: Reflection
222+
): CommentDisplayPart[] | undefined {
223+
if (comment instanceof Comment) {
224+
resolveLinks(comment, owner, this.validation, this.owner.logger);
225+
} else {
226+
let warned = false;
227+
const warn = () => {
228+
if (!warned) {
229+
warned = true;
230+
this.application.logger.warn(
231+
`${owner.name}: Comment [[target]] style links are deprecated and will be removed in 0.24`
232+
);
233+
}
234+
};
235+
236+
return resolvePartLinks(
237+
owner,
238+
comment,
239+
warn,
240+
this.validation,
241+
this.owner.logger
242+
);
243+
}
244+
}
245+
199246
/**
200247
* Compile the files within the given context and convert the compiler symbols to reflections.
201248
*

0 commit comments

Comments
 (0)