private _parseLinkTag()

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);
  }