private _parseFencedCode()

in tsdoc/src/parser/NodeParser.ts [2142:2379]


  private _parseFencedCode(tokenReader: TokenReader): DocNode {
    tokenReader.assertAccumulatedSequenceIsEmpty();

    const startMarker: number = tokenReader.createMarker();
    const endOfOpeningDelimiterMarker: number = startMarker + 2;

    switch (tokenReader.peekPreviousTokenKind()) {
      case TokenKind.Newline:
      case TokenKind.EndOfInput:
        break;
      default:
        return this._backtrackAndCreateErrorRange(
          tokenReader,
          startMarker,
          // include the three backticks so they don't get reinterpreted as a code span
          endOfOpeningDelimiterMarker,
          TSDocMessageId.CodeFenceOpeningIndent,
          'The opening backtick for a code fence must appear at the start of the line'
        );
    }

    // Read the opening ``` delimiter
    let openingDelimiter: string = '';
    openingDelimiter += tokenReader.readToken();
    openingDelimiter += tokenReader.readToken();
    openingDelimiter += tokenReader.readToken();

    if (openingDelimiter !== '```') {
      // This would be a parser bug -- the caller of _parseFencedCode() should have verified this while
      // looking ahead to distinguish code spans/fences
      throw new Error('Expecting three backticks');
    }

    const openingFenceExcerpt: TokenSequence = tokenReader.extractAccumulatedSequence();

    // Read any spaces after the delimiter,
    // but NOT the Newline since that goes with the spacingAfterLanguageExcerpt
    while (tokenReader.peekTokenKind() === TokenKind.Spacing) {
      tokenReader.readToken();
    }

    const spacingAfterOpeningFenceExcerpt:
      | TokenSequence
      | undefined = tokenReader.tryExtractAccumulatedSequence();

    // Read the language specifier (if present) and newline
    let done: boolean = false;
    let startOfPaddingMarker: number | undefined = undefined;
    while (!done) {
      switch (tokenReader.peekTokenKind()) {
        case TokenKind.Spacing:
        case TokenKind.Newline:
          if (startOfPaddingMarker === undefined) {
            // Starting a new run of spacing characters
            startOfPaddingMarker = tokenReader.createMarker();
          }
          if (tokenReader.peekTokenKind() === TokenKind.Newline) {
            done = true;
          }
          tokenReader.readToken();
          break;
        case TokenKind.Backtick:
          const failure: IFailure = this._createFailureForToken(
            tokenReader,
            TSDocMessageId.CodeFenceSpecifierSyntax,
            'The language specifier cannot contain backtick characters'
          );
          return this._backtrackAndCreateErrorRangeForFailure(
            tokenReader,
            startMarker,
            endOfOpeningDelimiterMarker,
            'Error parsing code fence: ',
            failure
          );
        case TokenKind.EndOfInput:
          const failure2: IFailure = this._createFailureForToken(
            tokenReader,
            TSDocMessageId.CodeFenceMissingDelimiter,
            'Missing closing delimiter'
          );
          return this._backtrackAndCreateErrorRangeForFailure(
            tokenReader,
            startMarker,
            endOfOpeningDelimiterMarker,
            'Error parsing code fence: ',
            failure2
          );
        default:
          // more non-spacing content
          startOfPaddingMarker = undefined;
          tokenReader.readToken();
          break;
      }
    }

    // At this point, we must have accumulated at least a newline token.
    // Example: "pov-ray sdl    \n"
    const restOfLineExcerpt: TokenSequence = tokenReader.extractAccumulatedSequence();

    // Example: "pov-ray sdl"
    const languageExcerpt: TokenSequence = restOfLineExcerpt.getNewSequence(
      restOfLineExcerpt.startIndex,
      startOfPaddingMarker!
    );

    // Example: "    \n"
    const spacingAfterLanguageExcerpt: TokenSequence | undefined = restOfLineExcerpt.getNewSequence(
      startOfPaddingMarker!,
      restOfLineExcerpt.endIndex
    );

    // Read the code content until we see the closing ``` delimiter
    let codeEndMarker: number = -1;
    let closingFenceStartMarker: number = -1;
    done = false;
    let tokenBeforeDelimiter: Token;
    while (!done) {
      switch (tokenReader.peekTokenKind()) {
        case TokenKind.EndOfInput:
          const failure2: IFailure = this._createFailureForToken(
            tokenReader,
            TSDocMessageId.CodeFenceMissingDelimiter,
            'Missing closing delimiter'
          );
          return this._backtrackAndCreateErrorRangeForFailure(
            tokenReader,
            startMarker,
            endOfOpeningDelimiterMarker,
            'Error parsing code fence: ',
            failure2
          );
        case TokenKind.Newline:
          tokenBeforeDelimiter = tokenReader.readToken();
          codeEndMarker = tokenReader.createMarker();

          while (tokenReader.peekTokenKind() === TokenKind.Spacing) {
            tokenBeforeDelimiter = tokenReader.readToken();
          }

          if (tokenReader.peekTokenKind() !== TokenKind.Backtick) {
            break;
          }
          closingFenceStartMarker = tokenReader.createMarker();
          tokenReader.readToken(); // first backtick

          if (tokenReader.peekTokenKind() !== TokenKind.Backtick) {
            break;
          }
          tokenReader.readToken(); // second backtick

          if (tokenReader.peekTokenKind() !== TokenKind.Backtick) {
            break;
          }
          tokenReader.readToken(); // third backtick

          done = true;
          break;
        default:
          tokenReader.readToken();
          break;
      }
    }

    if (tokenBeforeDelimiter!.kind !== TokenKind.Newline) {
      this._parserContext.log.addMessageForTextRange(
        TSDocMessageId.CodeFenceClosingIndent,
        'The closing delimiter for a code fence must not be indented',
        tokenBeforeDelimiter!.range
      );
    }

    // Example: "code 1\ncode 2\n  ```"
    const codeAndDelimiterExcerpt: TokenSequence = tokenReader.extractAccumulatedSequence();

    // Example: "code 1\ncode 2\n"
    const codeExcerpt: TokenSequence = codeAndDelimiterExcerpt.getNewSequence(
      codeAndDelimiterExcerpt.startIndex,
      codeEndMarker
    );

    // Example: "  "
    const spacingBeforeClosingFenceExcerpt:
      | TokenSequence
      | undefined = codeAndDelimiterExcerpt.getNewSequence(codeEndMarker, closingFenceStartMarker);

    // Example: "```"
    const closingFenceExcerpt: TokenSequence = codeAndDelimiterExcerpt.getNewSequence(
      closingFenceStartMarker,
      codeAndDelimiterExcerpt.endIndex
    );

    // Read the spacing and newline after the closing delimiter
    done = false;
    while (!done) {
      switch (tokenReader.peekTokenKind()) {
        case TokenKind.Spacing:
          tokenReader.readToken();
          break;
        case TokenKind.Newline:
          done = true;
          tokenReader.readToken();
          break;
        case TokenKind.EndOfInput:
          done = true;
          break;
        default:
          this._parserContext.log.addMessageForTextRange(
            TSDocMessageId.CodeFenceClosingSyntax,
            'Unexpected characters after closing delimiter for code fence',
            tokenReader.peekToken().range
          );
          done = true;
          break;
      }
    }

    // Example: "   \n"
    const spacingAfterClosingFenceExcerpt:
      | TokenSequence
      | undefined = tokenReader.tryExtractAccumulatedSequence();

    return new DocFencedCode({
      parsed: true,
      configuration: this._configuration,

      openingFenceExcerpt,
      spacingAfterOpeningFenceExcerpt,

      languageExcerpt,
      spacingAfterLanguageExcerpt,

      codeExcerpt,

      spacingBeforeClosingFenceExcerpt,
      closingFenceExcerpt,
      spacingAfterClosingFenceExcerpt
    });
  }