Skip to content
Open
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
259 changes: 106 additions & 153 deletions src/components/mdx/ApiLink/ApiLink.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,150 +2,88 @@
/**
* Inline API documentation link.
*
* Renders a <code> link pointing to the TypeDoc API reference for a class,
* interface, enum, type alias, variable, or function — and optionally a
* specific member (property / method) on a class or interface.
*
* Usage:
* <!-- core package — class (default kind) -->
* <ApiLink type="Toast" />
* → <a href="…/classes/igniteui-react.igrtoast.html"><code>IgrToast</code></a>
*
* <!-- class member -->
* <ApiLink type="Toast" member="show" label="Show" />
* → <a href="…/classes/igniteui-react.igrtoast.html#show"><code>Show</code></a>
*
* <!-- sub-package class -->
* <ApiLink pkg="charts" type="CategoryChart" />
*
* <!-- function (no platform prefix) -->
* <ApiLink kind="function" type="configureTheme" prefixed={false} />
* → <a href="…/functions/igniteui-react.configureTheme.html"><code>configureTheme</code></a>
*
* <!-- variable (fully-qualified) -->
* <ApiLink kind="variable" type="IgrCalendarResourceStringEN" prefixed={false} />
* → <a href="…/variables/igniteui-react.IgrCalendarResourceStringEN.html">…</a>
*
* <!-- type alias -->
* <ApiLink kind="type" type="AbsolutePosition" prefixed={false} />
* → <a href="…/types/igniteui-react.AbsolutePosition.html"><code>AbsolutePosition</code></a>
*
* <!-- interface -->
* <ApiLink kind="interface" type="ComboTemplateProps" prefixed={false} />
* → <a href="…/interfaces/igniteui-react.ComboTemplateProps.html">…</a>
*
* <!-- enum -->
* <ApiLink kind="enum" type="CalendarSelection" />
* → <a href="…/enums/igniteui-react.igrcalendarselection.html">…</a>
* TypeDoc links first resolve against the compact api-docs ApiLink index. If
* the index is unavailable, the component falls back to legacy URL generation
* so existing docs can migrate gradually.
*/
import type { PlatformContext } from '../../../lib/types.ts';
import type { ApiPackageConfig, PlatformContext } from '../../../lib/types.ts';
import {
KIND_SEGMENT,
resolveApiLinkFromIndex,
type TypeDocKind,
} from './api-link-index';
import './ApiLink.scss';

type ApiKind = 'class' | 'interface' | 'enum' | 'type' | 'variable' | 'function' | 'sass';

/** Props for SASS API documentation links (`kind="sass"`). */
type SassProps = {
kind: 'sass';
/** The anchor fragment without "#", e.g. "mixin-slide-in-left". Optional when linking to the module page. */
type?: string;
/** SASS module name, e.g. "animations", "themes". Required for correct URL generation. */
module?: string;
/**
* When true, wraps the label in <code>.
* @default true — matches TypeDoc ApiLink code-formatting behaviour.
* Set to false for descriptive/prose labels.
*/
code?: boolean;
/** Override the display text. Defaults to `type ?? module ?? ''`. */
label?: string;
};

/** Props for TypeDoc API documentation links (all non-sass kinds). */
type TypeDocProps = {
/**
* TypeDoc symbol kind. Determines the URL segment used.
* @default "class"
*/
kind?: Exclude<ApiKind, 'sass'>;
/**
* Short type/symbol name without platform prefix, e.g. "Toast".
* Required for all TypeDoc kinds.
*/
kind?: TypeDocKind;
type: string;
/** Optional member name (property / method), e.g. "show". Appended as #anchor. */
member?: string;
/**
* Package key as defined in platform-context apiPackages.
* Defaults to "core" (the main component package).
* Use "charts", "grids", "gauges", "maps", "inputs", "layouts",
* "excel", "spreadsheet", "datasources" for sub-packages.
*/
pkg?: string;
/**
* Override the display text. Defaults to the (prefixed) name,
* optionally suffixed with ".member".
*/
label?: string;
/**
* When true (default) the platform prefix (Igr/Igx/Igc/Igb) is
* prepended to `type` automatically. Set to false when passing a
* fully-qualified name or a non-prefixed symbol like a function name.
*/
prefixed?: boolean;
/**
* When true (default) the package classSuffix (e.g. "Component" for Angular)
* is appended to the class name. Set to false for utility/non-component classes
* that do not carry the platform class suffix (e.g. FilteringOperand, SortingStrategy).
* @default true
*/
suffix?: boolean;
/**
* Comma-separated list of platforms (e.g. "React" or "React,Blazor") for
* which the API link does NOT exist on the documentation site. When the
* current platform matches any entry the component renders the type name
* as plain inline code (no link), preserving the symbol reference for the
* reader without producing a broken URL.
*
* Use this instead of wrapping a single <ApiLink> in a <PlatformBlock>
* just to hide it from a specific platform.
*/
exclude?: string;
/**
* Comma-separated list of platforms for which the package classSuffix
* (e.g. "Component") should NOT be appended, overriding the package default.
* Useful when the same type is a plain class on some platforms but carries
* a framework suffix on others (e.g. "Angular,WebComponents").
*
* Does not suppress the link — combine with `exclude` to suppress entirely.
*/
excludeSuffixFor?: string;
/**
* Comma-separated list of platforms for which the platform prefix
* (Igr/Igx/Igc/Igb) should NOT be prepended, overriding the package default.
* Useful when a symbol has no prefix on certain platforms.
*
* Does not suppress the link — combine with `exclude` to suppress entirely.
*/
excludePrefixFor?: string;

};

type Props = SassProps | TypeDocProps;

const KIND_SEGMENT: Record<Exclude<ApiKind, 'sass'>, string> = {
class: 'classes',
interface: 'interfaces',
enum: 'enums',
type: 'types',
variable: 'variables',
function: 'functions',
};

const ctx = Astro.locals.platformContext as PlatformContext;
const { name: platformName, prefix, apiPackages } = ctx;
const label = Astro.props.label;

const splitList = (s?: string) => s ? s.split(',').map(p => p.trim()).filter(Boolean) : [];
const splitList = (value?: string) => value ? value.split(',').map(item => item.trim()).filter(Boolean) : [];
const upperFirst = (value: string) => value ? value.charAt(0).toUpperCase() + value.slice(1) : value;

function buildLegacyUrl(options: {
type: string;
kind: TypeDocKind;
member?: string;
prefix: string;
prefixed: boolean;
suffix: boolean;
pkgConfig: ApiPackageConfig;
}) {
const { type, kind, member, prefix, prefixed, suffix, pkgConfig } = options;
const baseType = prefixed ? `${prefix}${type}` : type;
const segment = KIND_SEGMENT[kind];

if (kind === 'class') {
const fullType = (suffix && pkgConfig.classSuffix) ? `${baseType}${pkgConfig.classSuffix}` : baseType;
const cased = pkgConfig.preserveCase ? fullType : fullType.toLowerCase();
const classSlug = pkgConfig.noPackagePrefix
? cased
: `${pkgConfig.packageId}.${cased}`;
const memberAnchor = member
? `#${pkgConfig.pascalCaseMembers ? upperFirst(member) : member}`
: '';
return `${pkgConfig.docRoot}/classes/${classSlug}${memberAnchor}`;
}

const slug = pkgConfig.noPackagePrefix
? baseType
: `${pkgConfig.packageId}.${baseType}`;
const memberAnchor = member
? `#${kind === 'enum' ? member : pkgConfig.pascalCaseMembers ? upperFirst(member) : member.toLowerCase()}`
: '';

return `${pkgConfig.docRoot}/${segment}/${slug}${memberAnchor}`;
}

function getIndexedDisplayName(resolvedName: string, fallbackName: string, type: string, classSuffix?: string) {
if (resolvedName === type) return type;
if (classSuffix && resolvedName === `${type}${classSuffix}`) return type;
return fallbackName;
}

let url: string;
let displayLabel: string;
Expand All @@ -154,11 +92,11 @@ let isExcluded = false;

if (Astro.props.kind === 'sass') {
const { type, module, code = true } = Astro.props;
if (!module) console.warn('[ApiLink] kind="sass" requires a `module` prop link may be malformed.');
if (!module) console.warn('[ApiLink] kind="sass" requires a `module` prop - link may be malformed.');
const base = ctx.sassApiUrl?.trim().replace(/\/+$/, '');
const anchor = type ? `#${type}` : '';
if (!base) {
console.warn('[ApiLink] kind="sass" requires `platformContext.sassApiUrl` to be configured falling back to "#".');
console.warn('[ApiLink] kind="sass" requires `platformContext.sassApiUrl` to be configured - falling back to "#".');
url = '#';
} else {
url = `${base}/${module ?? ''}${anchor}`;
Expand All @@ -167,46 +105,61 @@ if (Astro.props.kind === 'sass') {
renderCode = code;
} else {
const {
type, kind = 'class', member, pkg = 'core',
prefixed = true, suffix = true,
exclude, excludeSuffixFor, excludePrefixFor,
type,
member,
pkg = 'core',
prefixed = true,
suffix = true,
exclude,
excludeSuffixFor,
excludePrefixFor,
} = Astro.props;
const explicitKind = 'kind' in Astro.props ? Astro.props.kind : undefined;
const kind: TypeDocKind = explicitKind ?? 'class';

isExcluded = splitList(exclude).includes(platformName);
const isSuffixExcluded = splitList(excludeSuffixFor).includes(platformName);
const isPrefixExcluded = splitList(excludePrefixFor).includes(platformName);
const effectivePrefixed = prefixed && !isPrefixExcluded;
const effectiveSuffix = suffix && !isSuffixExcluded;

const pkgConfig = apiPackages[pkg] ?? apiPackages['core'];
const baseType = effectivePrefixed ? `${prefix}${type}` : type;
const segment = KIND_SEGMENT[kind];
if (kind === 'class') {
const fullType = (effectiveSuffix && pkgConfig.classSuffix) ? `${baseType}${pkgConfig.classSuffix}` : baseType;
const cased = pkgConfig.preserveCase ? fullType : fullType.toLowerCase();
const classSlug = pkgConfig.noPackagePrefix
? cased
: `${pkgConfig.packageId}.${cased}`;
const memberAnchor = member
? `#${pkgConfig.pascalCaseMembers ? member.charAt(0).toUpperCase() + member.slice(1) : member}`
: '';
url = `${pkgConfig.docRoot}/classes/${classSlug}${memberAnchor}`;
} else {
const slug = pkgConfig.noPackagePrefix
? baseType
: `${pkgConfig.packageId}.${baseType}`;
const memberAnchorNonClass = member
? `#${
kind === 'enum'
? member
: pkgConfig.pascalCaseMembers
? member.charAt(0).toUpperCase() + member.slice(1)
: member.toLowerCase()
}`
: '';
url = `${pkgConfig.docRoot}/${segment}/${slug}${memberAnchorNonClass}`;
}
const effectivePrefixed = prefixed && !splitList(excludePrefixFor).includes(platformName);
const effectiveSuffix = suffix && !splitList(excludeSuffixFor).includes(platformName);

// pkg is an ambiguity override. Without an explicit pkg prop, search the
// combined api-docs index so symbols can resolve from any package.
const explicitPkg = 'pkg' in Astro.props && typeof pkg === 'string' && pkg.length > 0;
const pkgConfig = apiPackages[explicitPkg ? pkg : 'core'] ?? apiPackages.core;
const baseType = effectivePrefixed ? `${prefix}${type}` : type;

url = buildLegacyUrl({
type,
kind,
member,
prefix,
prefixed: effectivePrefixed,
suffix: effectiveSuffix,
pkgConfig,
});
displayLabel = label ?? (member ? `${baseType}.${member}` : baseType);
renderCode = true;

if (!isExcluded) {
const indexed = await resolveApiLinkFromIndex({
ctx,
pkgConfig,
explicitPkg,
type,
member,
explicitKind,
prefix,
prefixed: effectivePrefixed,
suffix: effectiveSuffix,
});

if (indexed.status === 'resolved') {
url = indexed.url;
const indexedDisplay = getIndexedDisplayName(indexed.symbolName, baseType, type, pkgConfig.classSuffix);
displayLabel = label ?? (member ? `${indexedDisplay}.${member}` : indexedDisplay);
} else if (indexed.status === 'missing') {
isExcluded = true;
}
}
}
---
{isExcluded
Expand Down
Loading