Repository: angular/angular
File: adev/shared-docs/pipeline/shared/linking.mts (TypeScript)

Code before:
export function getSymbolUrl(symbol: string, apiEntries: ApiEntries): string | undefined {
  if (hasMoreThanOneDot(symbol) || !shouldLinkSymbol(symbol)) {
    return undefined;
  }

  const {symbolName, propName} = extractFromSymbol(symbol);

  // Try converting Angular selector pattern (ngXxx) to class name
  // Examples: ngCombobox -> Combobox, ngIf -> NgIf, ngFor -> NgFor
  if (symbolName.startsWith('ng') && symbolName.length > 2) {
    const withoutPrefix = symbolName.slice(2); // Remove 'ng'
    const capitalized = withoutPrefix.charAt(0).toUpperCase() + withoutPrefix.slice(1);

    // Try with Ng prefix first (e.g., ngIf -> NgIf, ngFor -> NgFor)
    const withNgPrefix = 'Ng' + capitalized;
    if (Object.hasOwn(apiEntries, withNgPrefix)) {
      const moduleName = apiEntries[withNgPrefix];
      return `/api/${moduleName}/${withNgPrefix}${propName ? `#${propName}` : ''}`;
    }

    // Try without Ng prefix (e.g., ngCombobox -> Combobox)
    if (Object.hasOwn(apiEntries, capitalized)) {
      const moduleName = apiEntries[capitalized];
      return `/api/${moduleName}/${capitalized}${propName ? `#${propName}` : ''}`;
    }
  }

  // We don't want to match entries like "constructor"
  const apiEntry = Object.hasOwn(apiEntries, symbolName) && apiEntries[symbolName];

  return apiEntry ? `/api/${apiEntry}/${symbolName}${propName ? `#${propName}` : ''}` : undefined;
}

function hasMoreThanOneDot(str: string) {
  return str.split('.').length - 1 > 1;
}


Review comment (suggestion):
This seems really to brittle. 

I was more thiking of changing the structure of `ApiEntries` to `Record<string, string|{moduleName: string, aliasTo?: string}>;`

Code after:
/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

// In some case we know that we don't want to link a symbol
// Example when there is a conflict between API entries and compiler features.
// eg: "animate" is both an Animation API entry and an template instruction "animation.enter"
// or "style" is a generic term but also an Animation API entry.
const LINK_EXEMPT = new Set(['animate', 'animate.enter', 'animate.leave', 'style']);

export function shouldLinkSymbol(symbol: string): boolean {
  return !LINK_EXEMPT.has(symbol);
}

// symbolName -> symbol info with moduleName and optional targetSymbol for aliases
export type ApiEntries = Record<string, {moduleName: string; targetSymbol?: string}>;

/**
 * Extracts the symbol name and property name from a symbol string.
 * eg:
 *   foobar() => {symbolName: 'foobar', propName: null}
 *   ApplicationRef.tick = > {symbolName: 'ApplicationRef', propName: 'tick'}
 *   ApplicationRef.tick() = > {symbolName: 'ApplicationRef', propName: 'tick'}
 *   @Component => {symbolName: 'Component', propName: null}
 */
export function extractFromSymbol(symbol: string): {propName: string | null; symbolName: string} {
  let propName: string | undefined;
  let symbolName = symbol;
  // we want to match functions when they have parentheses
  if (symbolName.endsWith('()')) {
    symbolName = symbolName.slice(0, -2);
  }

  if (symbolName.startsWith('@')) {
    symbolName = symbolName.slice(1);
  }
  if (symbolName.includes('#')) {
    [symbolName, propName] = symbolName.split('#');
  } else if (symbolName.includes('.')) {
    [symbolName, propName] = symbolName.split('.');
  }

  return {propName: propName ?? null, symbolName: symbolName};
}

export function getSymbolUrl(symbol: string, apiEntries: ApiEntries): string | undefined {
  if (hasMoreThanOneDot(symbol) || !shouldLinkSymbol(symbol)) {
    return undefined;
  }

  const {symbolName, propName} = extractFromSymbol(symbol);
  // We don't want to match entries like "constructor"
  if (!Object.hasOwn(apiEntries, symbolName)) {
    return undefined;
  }

  const apiEntry = apiEntries[symbolName];
  const moduleName = apiEntry.moduleName;
  const targetSymbol = apiEntry.targetSymbol ?? symbolName;

  return `/api/${moduleName}/${targetSymbol}${propName ? `#${propName}` : ''}`;
}

function hasMoreThanOneDot(str: string) {
  return str.split('.').length - 1 > 1;
}