in tsdoc/src/parser/NodeParser.ts [1018:1187]
private _parseLinkTag(
docInlineTagParsedParameters: IDocInlineTagParsedParameters,
embeddedTokenReader: TokenReader
): DocInlineTagBase {
// If an error occurs, then return a generic DocInlineTag instead of DocInheritDocTag
const errorTag: DocInlineTag = new DocInlineTag(docInlineTagParsedParameters);
const parameters: IDocLinkTagParsedParameters = {
...docInlineTagParsedParameters
};
if (!docInlineTagParsedParameters.tagContentExcerpt) {
this._parserContext.log.addMessageForTokenSequence(
TSDocMessageId.LinkTagEmpty,
'The @link tag content is missing',
parameters.tagNameExcerpt,
errorTag
);
return errorTag;
}
// Is the link destination a URL or a declaration reference?
//
// The JSDoc "@link" tag allows URLs, however supporting full URLs would be highly
// ambiguous, for example "microsoft.windows.camera:" is an actual valid URI scheme,
// and even the common "mailto:example.com" looks suspiciously like a declaration reference.
// In practice JSDoc URLs are nearly always HTTP or HTTPS, so it seems fairly reasonable to
// require the URL to have "://" and a scheme without any punctuation in it. If a more exotic
// URL is needed, the HTML "<a>" tag can always be used.
// We start with a fairly broad classifier heuristic, and then the parsers will refine this:
// 1. Does it start with "//"?
// 2. Does it contain "://"?
let looksLikeUrl: boolean =
embeddedTokenReader.peekTokenKind() === TokenKind.Slash &&
embeddedTokenReader.peekTokenAfterKind() === TokenKind.Slash;
const marker: number = embeddedTokenReader.createMarker();
let done: boolean = looksLikeUrl;
while (!done) {
switch (embeddedTokenReader.peekTokenKind()) {
// An URI scheme can contain letters, numbers, minus, plus, and periods
case TokenKind.AsciiWord:
case TokenKind.Period:
case TokenKind.Hyphen:
case TokenKind.Plus:
embeddedTokenReader.readToken();
break;
case TokenKind.Colon:
embeddedTokenReader.readToken();
// Once we a reach a colon, then it's a URL only if we see "://"
looksLikeUrl =
embeddedTokenReader.peekTokenKind() === TokenKind.Slash &&
embeddedTokenReader.peekTokenAfterKind() === TokenKind.Slash;
done = true;
break;
default:
done = true;
}
}
embeddedTokenReader.backtrackToMarker(marker);
// Is the hyperlink a URL or a declaration reference?
if (looksLikeUrl) {
// It starts with something like "http://", so parse it as a URL
if (
!this._parseLinkTagUrlDestination(
embeddedTokenReader,
parameters,
docInlineTagParsedParameters.tagNameExcerpt,
errorTag
)
) {
return errorTag;
}
} else {
// Otherwise, assume it's a declaration reference
if (
!this._parseLinkTagCodeDestination(
embeddedTokenReader,
parameters,
docInlineTagParsedParameters.tagNameExcerpt,
errorTag
)
) {
return errorTag;
}
}
if (embeddedTokenReader.peekTokenKind() === TokenKind.Spacing) {
// The above parser rules should have consumed any spacing before the pipe
throw new Error('Unconsumed spacing encountered after construct');
}
if (embeddedTokenReader.peekTokenKind() === TokenKind.Pipe) {
// Read the link text
embeddedTokenReader.readToken();
parameters.pipeExcerpt = embeddedTokenReader.extractAccumulatedSequence();
parameters.spacingAfterPipeExcerpt = this._tryReadSpacingAndNewlines(embeddedTokenReader);
// Read everything until the end
// NOTE: Because we're using an embedded TokenReader, the TokenKind.EndOfInput occurs
// when we reach the "}", not the end of the original input
done = false;
let spacingAfterLinkTextMarker: number | undefined = undefined;
while (!done) {
switch (embeddedTokenReader.peekTokenKind()) {
case TokenKind.EndOfInput:
done = true;
break;
case TokenKind.Pipe:
case TokenKind.LeftCurlyBracket:
const badCharacter: string = embeddedTokenReader.readToken().toString();
this._parserContext.log.addMessageForTokenSequence(
TSDocMessageId.LinkTagUnescapedText,
`The "${badCharacter}" character may not be used in the link text without escaping it`,
embeddedTokenReader.extractAccumulatedSequence(),
errorTag
);
return errorTag;
case TokenKind.Spacing:
case TokenKind.Newline:
embeddedTokenReader.readToken();
break;
default:
// We found a non-spacing character, so move the spacingAfterLinkTextMarker
spacingAfterLinkTextMarker = embeddedTokenReader.createMarker() + 1;
embeddedTokenReader.readToken();
}
}
const linkTextAndSpacing:
| TokenSequence
| undefined = embeddedTokenReader.tryExtractAccumulatedSequence();
if (linkTextAndSpacing) {
if (spacingAfterLinkTextMarker === undefined) {
// We never found any non-spacing characters, so everything is trailing spacing
parameters.spacingAfterLinkTextExcerpt = linkTextAndSpacing;
} else if (spacingAfterLinkTextMarker >= linkTextAndSpacing.endIndex) {
// We found no trailing spacing, so everything we found is the text
parameters.linkTextExcerpt = linkTextAndSpacing;
} else {
// Split the trailing spacing from the link text
parameters.linkTextExcerpt = linkTextAndSpacing.getNewSequence(
linkTextAndSpacing.startIndex,
spacingAfterLinkTextMarker
);
parameters.spacingAfterLinkTextExcerpt = linkTextAndSpacing.getNewSequence(
spacingAfterLinkTextMarker,
linkTextAndSpacing.endIndex
);
}
}
} else if (embeddedTokenReader.peekTokenKind() !== TokenKind.EndOfInput) {
embeddedTokenReader.readToken();
this._parserContext.log.addMessageForTokenSequence(
TSDocMessageId.LinkTagDestinationSyntax,
'Unexpected character after link destination',
embeddedTokenReader.extractAccumulatedSequence(),
errorTag
);
return errorTag;
}
return new DocLinkTag(parameters);
}