Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,17 @@ import {
ApiFunction,
ApiItem,
ApiItemKind,
ExcerptToken,
ExcerptTokenKind,
Parameter,
TypeParameter,
} from "@microsoft/api-extractor-model";
import { ReviewToken, TokenKind } from "../models";
import { TokenGenerator } from "./index";
import { createToken, processExcerptTokens } from "./helpers";

function isValid(item: ApiItem): item is ApiFunction {
return item.kind === ApiItemKind.Function;
}

/** Helper to create a token with common properties */
function createToken(
kind: TokenKind,
value: string,
options?: {
hasSuffixSpace?: boolean;
hasPrefixSpace?: boolean;
navigateToId?: string;
deprecated?: boolean;
},
): ReviewToken {
return {
Kind: kind,
Value: value,
HasSuffixSpace: options?.hasSuffixSpace ?? false,
HasPrefixSpace: options?.hasPrefixSpace ?? false,
NavigateToId: options?.navigateToId,
IsDeprecated: options?.deprecated,
};
}

/**
* Determines if a token needs a leading space based on its value
* @param value The token value
* @returns true if the token needs a leading space
*/
function needsLeadingSpace(value: string): boolean {
return value === "|" || value === "&" || value === "is";
}

/**
* Determines if a token needs a trailing space based on its value
* @param value The token value
* @returns true if the token needs a trailing space
*/
function needsTrailingSpace(value: string): boolean {
return value === "|" || value === "&" || value === "is";
}

/** Process excerpt tokens and add them to the tokens array */
function processExcerptTokens(
excerptTokens: readonly ExcerptToken[],
tokens: ReviewToken[],
deprecated?: boolean,
): void {
for (const excerpt of excerptTokens) {
const trimmedText = excerpt.text.trim();
if (!trimmedText) continue;

const hasPrefixSpace = needsLeadingSpace(trimmedText);
const hasSuffixSpace = needsTrailingSpace(trimmedText);

if (excerpt.kind === ExcerptTokenKind.Reference && excerpt.canonicalReference) {
tokens.push(
createToken(TokenKind.TypeName, trimmedText, {
navigateToId: excerpt.canonicalReference.toString(),
hasPrefixSpace,
hasSuffixSpace,
deprecated,
}),
);
} else {
tokens.push(
createToken(TokenKind.Text, trimmedText, {
hasPrefixSpace,
hasSuffixSpace,
deprecated,
}),
);
}
}
}

function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
const tokens: ReviewToken[] = [];
if (item.kind !== ApiItemKind.Function) {
Expand All @@ -96,11 +22,8 @@ function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
}

// Extract structured properties
const parameters = (item as unknown as { readonly parameters: ReadonlyArray<Parameter> })
.parameters;
const typeParameters = (
item as unknown as { readonly typeParameters: ReadonlyArray<TypeParameter> }
).typeParameters;
const parameters = item.parameters;
const typeParameters = item.typeParameters;

// Add export and function keywords
tokens.push(createToken(TokenKind.Keyword, "export", { hasSuffixSpace: true, deprecated }));
Expand Down Expand Up @@ -131,6 +54,17 @@ function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
tokens.push(createToken(TokenKind.Text, tp.constraintExcerpt.text.trim(), { deprecated }));
}

if (tp.defaultTypeExcerpt?.text.trim()) {
tokens.push(
createToken(TokenKind.Text, "=", {
hasPrefixSpace: true,
hasSuffixSpace: true,
deprecated,
}),
);
processExcerptTokens(tp.defaultTypeExcerpt.spannedTokens, tokens, deprecated);
}

if (index < typeParameters.length - 1) {
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ExcerptToken, ExcerptTokenKind } from "@microsoft/api-extractor-model";
import { ReviewToken, TokenKind } from "../models";

/** Helper to create a token with common properties */
export function createToken(
kind: TokenKind,
value: string,
options?: {
hasSuffixSpace?: boolean;
hasPrefixSpace?: boolean;
navigateToId?: string;
deprecated?: boolean;
},
): ReviewToken {
return {
Kind: kind,
Value: value,
HasSuffixSpace: options?.hasSuffixSpace ?? false,
HasPrefixSpace: options?.hasPrefixSpace ?? false,
NavigateToId: options?.navigateToId,
IsDeprecated: options?.deprecated,
};
}

/**
* Determines if a token needs a leading space based on its value
* @param value The token value
* @returns true if the token needs a leading space
*/
export function needsLeadingSpace(value: string): boolean {
return value === "|" || value === "&" || value === "is" || value === "extends";
}

/**
* Determines if a token needs a trailing space based on its value
* @param value The token value
* @returns true if the token needs a trailing space
*/
export function needsTrailingSpace(value: string): boolean {
return value === "|" || value === "&" || value === "is" || value === "extends";
}

/** Process excerpt tokens and add them to the tokens array */
export function processExcerptTokens(
excerptTokens: readonly ExcerptToken[],
tokens: ReviewToken[],
deprecated?: boolean,
): void {
for (const excerpt of excerptTokens) {
const trimmedText = excerpt.text.trim();
if (!trimmedText) continue;

const hasPrefixSpace = needsLeadingSpace(trimmedText);
const hasSuffixSpace = needsTrailingSpace(trimmedText);

if (excerpt.kind === ExcerptTokenKind.Reference && excerpt.canonicalReference) {
tokens.push(
createToken(TokenKind.TypeName, trimmedText, {
navigateToId: excerpt.canonicalReference.toString(),
hasPrefixSpace,
hasSuffixSpace,
deprecated,
}),
);
} else {
tokens.push(
createToken(TokenKind.Text, trimmedText, {
hasPrefixSpace,
hasSuffixSpace,
deprecated,
}),
);
}
}
}
38 changes: 20 additions & 18 deletions tools/apiview/parsers/js-api-parser/src/tokenGenerators/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { enumTokenGenerator } from "./enum";
import { ReviewToken } from '../models';
import { ApiItem } from '@microsoft/api-extractor-model';
import { ReviewToken } from "../models";
import { ApiItem } from "@microsoft/api-extractor-model";
import { functionTokenGenerator } from "./function";
import { interfaceTokenGenerator } from "./interfaces";

/**
* Interface for token generators that create ReviewTokens from ApiItems.
*/
export interface TokenGenerator<T extends ApiItem = ApiItem> {
/**
* Validates if the given ApiItem can be processed by this token generator.
* @param item - The ApiItem to validate.
* @returns True if the item is valid; otherwise, false.
*/
isValid(item: ApiItem): item is T;
/**
* Validates if the given ApiItem can be processed by this token generator.
* @param item - The ApiItem to validate.
* @returns True if the item is valid; otherwise, false.
*/
isValid(item: ApiItem): item is T;

/**
* Generates ReviewTokens from the given ApiItem.
* @param item - The ApiItem to process.
* @param deprecated - Indicates if the Api is deprecated.
* @returns An array of ReviewTokens generated from the ApiItem.
*/
generate(item: T, deprecated?: boolean): ReviewToken[];
/**
* Generates ReviewTokens from the given ApiItem.
* @param item - The ApiItem to process.
* @param deprecated - Indicates if the Api is deprecated.
* @returns An array of ReviewTokens generated from the ApiItem.
*/
generate(item: T, deprecated?: boolean): ReviewToken[];
}

export const generators: TokenGenerator[] = [
enumTokenGenerator,
functionTokenGenerator
];
enumTokenGenerator,
functionTokenGenerator,
interfaceTokenGenerator,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ApiInterface, ApiItem, ApiItemKind } from "@microsoft/api-extractor-model";
import { ReviewToken, TokenKind } from "../models";
import { TokenGenerator } from "./index";
import { createToken, processExcerptTokens } from "./helpers";

function isValid(item: ApiItem): item is ApiInterface {
return item.kind === ApiItemKind.Interface;
}

function generate(item: ApiInterface, deprecated?: boolean): ReviewToken[] {
const tokens: ReviewToken[] = [];
if (item.kind !== ApiItemKind.Interface) {
throw new Error(
`Invalid item ${item.displayName} of kind ${item.kind} for Interface token generator.`,
);
}

// Extract structured properties
const typeParameters = item.typeParameters;

// Add export and interface keywords
tokens.push(createToken(TokenKind.Keyword, "export", { hasSuffixSpace: true, deprecated }));

// Check for default export
const isDefaultExport = item.excerptTokens.some((t) => t.text.includes("export default"));
if (isDefaultExport) {
tokens.push(createToken(TokenKind.Keyword, "default", { hasSuffixSpace: true, deprecated }));
}

tokens.push(createToken(TokenKind.Keyword, "interface", { hasSuffixSpace: true, deprecated }));

// Create interface name token with proper metadata (matching splitAndBuild behavior)
const nameToken = createToken(TokenKind.TypeName, item.displayName, { deprecated });
nameToken.NavigateToId = item.canonicalReference.toString();
nameToken.NavigationDisplayName = item.displayName;
nameToken.RenderClasses = ["interface"];
tokens.push(nameToken);

// Add type parameters
if (typeParameters?.length > 0) {
tokens.push(createToken(TokenKind.Text, "<", { deprecated }));
typeParameters.forEach((tp, index) => {
tokens.push(createToken(TokenKind.TypeName, tp.name, { deprecated }));

if (tp.constraintExcerpt?.text.trim()) {
tokens.push(
createToken(TokenKind.Keyword, "extends", {
hasPrefixSpace: true,
hasSuffixSpace: true,
deprecated,
}),
);
processExcerptTokens(tp.constraintExcerpt.spannedTokens, tokens, deprecated);
}

if (tp.defaultTypeExcerpt?.text.trim()) {
tokens.push(
createToken(TokenKind.Text, "=", {
hasPrefixSpace: true,
hasSuffixSpace: true,
deprecated,
}),
);
processExcerptTokens(tp.defaultTypeExcerpt.spannedTokens, tokens, deprecated);
}

if (index < typeParameters.length - 1) {
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
}
});
tokens.push(createToken(TokenKind.Text, ">", { deprecated }));
}

// Add extends clause if interface extends other interfaces
if (item.extendsTypes && item.extendsTypes.length > 0) {
tokens.push(
createToken(TokenKind.Keyword, "extends", {
hasPrefixSpace: true,
hasSuffixSpace: true,
deprecated,
}),
);

item.extendsTypes.forEach((extendsType, index) => {
processExcerptTokens(extendsType.excerpt.spannedTokens, tokens, deprecated);

if (index < item.extendsTypes.length - 1) {
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
}
});
}

return tokens;
}

export const interfaceTokenGenerator: TokenGenerator<ApiInterface> = {
isValid,
generate,
};
Loading
Loading