UNPKG

@microsoft/api-extractor

Version:

Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups

102 lines 6.44 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import { ExcerptTokenKind } from '@microsoft/api-extractor-model'; /** * Condenses the provided excerpt tokens by merging tokens where possible. Updates the provided token ranges to * remain accurate after token merging. * * @remarks * For example, suppose we have excerpt tokens ["A", "B", "C"] and a token range [0, 2]. If the excerpt tokens * are condensed to ["AB", "C"], then the token range would be updated to [0, 1]. Note that merges are only * performed if they are compatible with the provided token ranges. In the example above, if our token range was * originally [0, 1], we would not be able to merge tokens "A" and "B". */ export function condenseTokens(excerptTokens, tokenRanges) { const originalTokenCount = excerptTokens.length; // A token that sits at the start or end index of any token range must be preserved (never merged // away), so that every range can be accurately remapped after condensing. These indices refer to // the original positions in `excerptTokens`, and since preserved tokens are never removed, this // set never needs to be rebuilt. const preservedIndices = new Set(); for (const tokenRange of tokenRanges) { preservedIndices.add(tokenRange.startIndex); preservedIndices.add(tokenRange.endIndex); } // Build the condensed token list in a single forward pass, treating it as a stack so that a // token merged into its predecessor can itself be merged into a further predecessor (e.g. a // chain of reference tokens such as "A" "." "B" "." "C"). // // `newIndexByOriginalIndex` maps each kept token's original index to its index in the condensed // list, which is then used to remap the token ranges. Every range boundary refers to a preserved // token (or, for an exclusive `endIndex`, the token count), and preserved tokens are never merged // away, so a direct lookup always resolves. It is sized `originalTokenCount + 1` to hold the // mapping for an `endIndex` equal to the token count. const condensedTokens = []; const newIndexByOriginalIndex = new Int32Array(originalTokenCount + 1); // Whether the token currently on top of the stack is preserved. Only consulted when that token is // the "." of a reference merge; because a "." only ever becomes the top via a push (content tokens // are always separated by references, so a "." is never uncovered by a pop), this scalar is always // up to date at the point it is read, avoiding a parallel array of original indices. let prevTokenIsPreserved = false; for (let currentIndex = 0; currentIndex < originalTokenCount; ++currentIndex) { const currentToken = excerptTokens[currentIndex]; const currentIsPreserved = preservedIndices.has(currentIndex); const condensedCount = condensedTokens.length; // A preserved token must never be merged away, so merges are only attempted when the current // token is not preserved. There are two types of merges that can occur, and both consume the // current token. Reads of the top two stack entries are guarded so they never index out of // bounds. let merged = false; if (!currentIsPreserved && condensedCount >= 1) { const prevToken = condensedTokens[condensedCount - 1]; if (condensedCount >= 2 && currentToken.kind === ExcerptTokenKind.Reference && prevToken.kind === ExcerptTokenKind.Content && prevToken.text.trim() === '.' && !prevTokenIsPreserved && condensedTokens[condensedCount - 2].kind === ExcerptTokenKind.Reference) { // If the current token is a reference token, the previous token is a ".", and the previous- // previous token is a reference token, then merge all three tokens into a reference token. // // For example: Given ["MyNamespace" (R), ".", "MyClass" (R)], tokens "." and "MyClass" might // be merged into "MyNamespace". The condensed token would be ["MyNamespace.MyClass" (R)]. const prevPrevToken = condensedTokens[condensedCount - 2]; prevPrevToken.text += prevToken.text + currentToken.text; prevPrevToken.canonicalReference = currentToken.canonicalReference; // The "." token (already kept) and the current token are both merged into prevPrevToken. condensedTokens.pop(); merged = true; } else if ( // If the current and previous tokens are both content tokens, then merge the tokens into a // single content token. For example: Given ["export ", "declare class"], these tokens // might be merged into "export declare class". prevToken.kind === ExcerptTokenKind.Content && currentToken.kind === ExcerptTokenKind.Content) { prevToken.text += currentToken.text; merged = true; } } if (!merged) { // No merging occurred, so keep the current token, record its new index, and update the // preservation flag to reflect the new top of the stack. newIndexByOriginalIndex[currentIndex] = condensedTokens.length; condensedTokens.push(currentToken); prevTokenIsPreserved = currentIsPreserved; } } // Remap the token ranges directly. Each boundary is a preserved token's original index (or the // token count, for an exclusive `endIndex`), which maps straight to its position in the condensed // list. `endIndex` is clamped because it may equal the token count. newIndexByOriginalIndex[originalTokenCount] = condensedTokens.length; for (const tokenRange of tokenRanges) { tokenRange.startIndex = newIndexByOriginalIndex[Math.min(tokenRange.startIndex, originalTokenCount)]; tokenRange.endIndex = newIndexByOriginalIndex[Math.min(tokenRange.endIndex, originalTokenCount)]; } // Replace the excerpt tokens in place with the condensed list. excerptTokens.length = 0; for (const token of condensedTokens) { excerptTokens.push(token); } } //# sourceMappingURL=condenseTokens.js.map