@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
231 lines (229 loc) • 11.5 kB
JavaScript
;
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
const Markup_1 = require("./markup/Markup");
const ApiDefinitionReference_1 = require("./ApiDefinitionReference");
const AstItem_1 = require("./ast/AstItem");
const Token_1 = require("./aedoc/Token");
class DocElementParser {
static parse(documentation, tokenizer) {
const markupElements = [];
let parsing = true;
let token;
while (parsing) {
token = tokenizer.peekToken();
if (!token) {
parsing = false; // end of stream
break;
}
if (token.type === Token_1.TokenType.BlockTag) {
parsing = false; // end of summary tokens
}
else if (token.type === Token_1.TokenType.InlineTag) {
switch (token.tag) {
case '@inheritdoc':
tokenizer.getToken();
if (markupElements.length > 0 || documentation.summary.length > 0) {
documentation.reportError('A summary block is not allowed here,'
+ ' because the @inheritdoc target provides the summary');
}
documentation.incompleteInheritdocs.push(token);
documentation.isDocInherited = true;
break;
case '@link':
const linkMarkupElement = this.parseLinkTag(documentation, token);
if (linkMarkupElement) {
// Push to linkMarkupElement to retain position in the documentation
markupElements.push(linkMarkupElement);
if (linkMarkupElement.kind === 'api-link') {
documentation.incompleteLinks.push(linkMarkupElement);
}
}
tokenizer.getToken(); // get the link token
break;
default:
parsing = false;
break;
}
}
else if (token.type === Token_1.TokenType.Text) {
tokenizer.getToken();
markupElements.push(...Markup_1.Markup.createTextParagraphs(token.text));
}
else {
documentation.reportError(`Unidentifiable Token ${token.type} ${token.tag} "${token.text}"`);
}
}
return markupElements;
}
static parseAndNormalize(documentation, tokenizer) {
const markupElements = DocElementParser.parse(documentation, tokenizer);
Markup_1.Markup.normalize(markupElements);
return markupElements;
}
/**
* This method parses the semantic information in an \@link JSDoc tag, creates and returns a
* MarkupElement with the corresponding information. If the corresponding inline tag \@link is
* not formatted correctly an error will be reported and undefined is returned.
*
* The format for the \@link tag is {\@link URL or API defintion reference | display text}, where
* the '|' is only needed if the optional display text is given.
*
* Examples:
* \{@link http://microsoft.com | microsoft home \}
* \{@link http://microsoft.com \}
* \{@link @microsoft/sp-core-library:Guid.newGuid | new Guid Object \}
* \{@link @microsoft/sp-core-library:Guid.newGuid \}
*/
static parseLinkTag(documentation, tokenItem) {
if (!tokenItem.text) {
documentation.reportError('The {@link} tag must include a URL or API item reference');
return undefined;
}
// Make sure there are no extra pipes
const pipeSplitContent = tokenItem.text.split('|').map(value => {
return value ? value.trim() : value;
});
if (pipeSplitContent.length > 2) {
documentation.reportError('The {@link} tag contains more than one pipe character ("|")');
return undefined;
}
const addressPart = pipeSplitContent[0];
const displayTextPart = pipeSplitContent.length > 1 ? pipeSplitContent[1] : '';
let displayTextElements;
// If a display name is given, ensure it only contains characters for words.
if (displayTextPart) {
const match = this._displayTextBadCharacterRegEx.exec(displayTextPart);
if (match) {
documentation.reportError(`The {@link} tag\'s display text contains an unsupported`
+ ` character: "${match[0]}"`);
return undefined;
}
// Full match is valid text
displayTextElements = Markup_1.Markup.createTextElements(displayTextPart);
}
else {
// If the display text is not explicitly provided, then use the address as the display text
displayTextElements = Markup_1.Markup.createTextElements(addressPart);
}
// Try to guess if the tokenContent is a link or API definition reference
let linkMarkupElement;
if (this._hrefRegEx.test(addressPart)) {
// Make sure only a single URL is given
if (addressPart.indexOf(' ') >= 0) {
documentation.reportError('The {@link} tag contains additional spaces after the URL;'
+ ' if the URL contains spaces, encode them using %20; for display text, use a pipe delimiter ("|")');
return undefined;
}
linkMarkupElement = Markup_1.Markup.createWebLink(displayTextElements, addressPart);
}
else {
// we are processing an API definition reference
const apiDefitionRef = ApiDefinitionReference_1.default.createFromString(addressPart, documentation.reportError);
// Once we can locate local API definitions, an error should be reported here if not found.
if (!apiDefitionRef) {
return undefined;
}
const normalizedApiLink = apiDefitionRef.toApiItemReference();
if (!normalizedApiLink.packageName) {
if (!documentation.context.packageName) {
throw new Error('Unable to resolve API reference without a package name');
}
// If the package name is unspecified, assume it is the current package
const scopePackageName = ApiDefinitionReference_1.default.parseScopedPackageName(documentation.context.packageName);
normalizedApiLink.scopeName = scopePackageName.scope;
normalizedApiLink.packageName = scopePackageName.package;
}
linkMarkupElement = Markup_1.Markup.createApiLink(displayTextElements, normalizedApiLink);
}
return linkMarkupElement;
}
/**
* This method parses the semantic information in an \@inheritdoc JSDoc tag and sets
* all the relevant documenation properties from the inherited doc onto the documenation
* of the current api item.
*
* The format for the \@inheritdoc tag is {\@inheritdoc scopeName/packageName:exportName.memberName}.
* For more information on the format see IInheritdocRef.
*/
static parseInheritDoc(documentation, token, warnings) {
// Check to make sure the API definition reference is at most one string
const tokenChunks = token.text.split(' ');
if (tokenChunks.length > 1) {
documentation.reportError('The {@inheritdoc} tag does not match the expected pattern' +
' "{@inheritdoc @scopeName/packageName:exportName}"');
return;
}
// Create the IApiDefinitionReference object
// Deconstruct the API reference expression 'scopeName/packageName:exportName.memberName'
const apiDefinitionRef = ApiDefinitionReference_1.default.createFromString(token.text, documentation.reportError);
// if API reference expression is formatted incorrectly then apiDefinitionRef will be undefined
if (!apiDefinitionRef) {
documentation.reportError(`Incorrectly formatted API item reference: "${token.text}"`);
return;
}
// Atempt to locate the apiDefinitionRef
const resolvedAstItem = documentation.referenceResolver.resolve(apiDefinitionRef, documentation.context.package, warnings);
// If no resolvedAstItem found then nothing to inherit
// But for the time being set the summary to a text object
if (!resolvedAstItem) {
documentation.summary = Markup_1.Markup.createTextElements(`See documentation for ${tokenChunks[0]}`);
return;
}
// We are going to copy the resolvedAstItem's documentation
// We must make sure it's documentation can be completed,
// if we cannot, an error will be reported viathe documentation error handler.
// This will only be the case our resolvedAstItem was created from a local
// AstItem. Resolutions from JSON will have an undefined 'astItem' property.
// Example: a circular reference will report an error.
if (resolvedAstItem.astItem) {
resolvedAstItem.astItem.completeInitialization();
}
// inheritdoc found, copy over IApiBaseDefinition properties
documentation.summary = resolvedAstItem.summary;
documentation.remarks = resolvedAstItem.remarks;
// Copy over detailed properties if neccessary
// Add additional cases if needed
switch (resolvedAstItem.kind) {
case AstItem_1.AstItemKind.Function:
documentation.parameters = resolvedAstItem.params || {};
documentation.returnsMessage = resolvedAstItem.returnsMessage || [];
break;
case AstItem_1.AstItemKind.Method:
case AstItem_1.AstItemKind.Constructor:
documentation.parameters = resolvedAstItem.params || {};
documentation.returnsMessage = resolvedAstItem.returnsMessage || [];
break;
}
// Check if inheritdoc is depreacted
// We need to check if this documentation has a deprecated message
// but it may not appear until after this token.
if (resolvedAstItem.deprecatedMessage && resolvedAstItem.deprecatedMessage.length > 0) {
documentation.isDocInheritedDeprecated = true;
}
}
}
/**
* Used to validate the display text for an \@link tag. The display text can contain any
* characters except for certain AEDoc delimiters: "@", "|", "{", "}".
* This RegExp matches the first bad character.
* Example: "Microsoft's {spec}" --> "{"
*/
DocElementParser._displayTextBadCharacterRegEx = /[@|{}]/;
/**
* Matches a href reference. This is used to get an idea whether a given reference is for an href
* or an API definition reference.
*
* For example, the following would be matched:
* 'http://'
* 'https://'
*
* The following would not be matched:
* '@microsoft/sp-core-library:Guid.newGuid'
* 'Guid.newGuid'
* 'Guid'
*/
DocElementParser._hrefRegEx = /^[a-z]+:\/\//;
exports.default = DocElementParser;
//# sourceMappingURL=DocElementParser.js.map