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