Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
6 changes: 0 additions & 6 deletions playground/src/pages/components/api-link.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const headings = [
{ depth: 2, slug: 'function', text: 'Function' },
{ depth: 2, slug: 'type-alias', text: 'Type alias' },
{ depth: 2, slug: 'sub-package', text: 'Sub-package' },
{ depth: 2, slug: 'platform-overrides', text: 'Platform overrides' },
{ depth: 2, slug: 'sass-links', text: 'Sass links' },
{ depth: 2, slug: 'platform-env', text: 'Platform env var' },
];
Expand Down Expand Up @@ -49,11 +48,6 @@ const headings = [
<h2 id="sub-package">Sub-package</h2>
<p>Use the <code>pkg</code> prop to target a sub-package: <ApiLink pkg="charts" type="CategoryChart" /></p>

<h2 id="platform-overrides">Platform overrides</h2>
<p><code>exclude</code> renders plain code on matching platforms: <ApiLink type="Toast" member="show" exclude="React" /></p>
<p><code>excludePrefixFor</code> keeps symbols unprefixed on matching platforms: <ApiLink pkg="charts" type="ChartSelection" excludePrefixFor="React" /></p>
<p><code>excludeSuffixFor</code> suppresses framework class suffixes on matching platforms: <ApiLink pkg="charts" type="ChartSelection" excludeSuffixFor="React" /></p>

<h2 id="sass-links">Sass links</h2>
<p>Sass module page with prose label: <ApiLink kind="sass" module="animations" label="animations Sass module" code={false} /></p>
<p>Sass API anchor with code label: <ApiLink kind="sass" module="animations" type="mixin-slide-in-left" label="slide-in-left" /></p>
Expand Down
268 changes: 118 additions & 150 deletions src/components/mdx/ApiLink/ApiLink.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,163 +2,124 @@
/**
* 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"`). */
/** Props for Sass API documentation links (`kind="sass"`). */
type SassProps = {
/** Selects Sass API link mode. */
kind: 'sass';
/** The anchor fragment without "#", e.g. "mixin-slide-in-left". Optional when linking to the module page. */
Comment thread
viktorkombov marked this conversation as resolved.
/** Anchor fragment without `#`, e.g. `mixin-slide-in-left`. */
type?: string;
/** SASS module name, e.g. "animations", "themes". Required for correct URL generation. */
/** Sass module path segment, e.g. `animations` or `themes`. */
module?: string;
/**
* When true, wraps the label in <code>.
* @default true — matches TypeDoc ApiLink code-formatting behaviour.
* Set to false for descriptive/prose labels.
*/
/** Wrap the rendered label in `<code>`. Defaults to `true`. */
code?: boolean;
/** Override the display text. Defaults to `type ?? module ?? ''`. */
/** Override the rendered label. */
label?: string;
};

/** Props for TypeDoc API documentation links (all non-sass kinds). */
/** Props for TypeDoc API documentation links. */
type TypeDocProps = {
/**
* TypeDoc symbol kind. Determines the URL segment used.
* TypeDoc symbol kind. Narrows registry lookup and determines the legacy
* fallback URL segment.
* @default "class"
*/
kind?: Exclude<ApiKind, 'sass'>;
kind?: TypeDocKind;
/**
* Short type/symbol name without platform prefix, e.g. "Toast".
* Required for all TypeDoc kinds.
*/
type: string;
Comment thread
viktorkombov marked this conversation as resolved.
/** Optional member name (property / method), e.g. "show". Appended as #anchor. */
/** Optional class/interface member, property, method, or enum value 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.
*/
/** Package key used only to filter ambiguous registry matches. */
pkg?: string;
/**
* Override the display text. Defaults to the (prefixed) name,
* optionally suffixed with ".member".
*/
/** Override the rendered label. */
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.
* Whether to prepend the platform prefix before registry lookup or legacy
* fallback URL generation.
* @default true
*/
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).
* Whether to try the platform class suffix before the unsuffixed symbol.
* @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;

};
Comment thread
viktorkombov marked this conversation as resolved.

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 { prefix, apiPackages } = ctx;
const label = Astro.props.label;

const splitList = (s?: string) => s ? s.split(',').map(p => p.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;
let renderCode: boolean;
let isExcluded = false;

Comment thread
viktorkombov marked this conversation as resolved.
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,48 +128,55 @@ 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,
} = Astro.props;
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 explicitKind = 'kind' in Astro.props ? Astro.props.kind : undefined;
const kind: TypeDocKind = explicitKind ?? 'class';

const effectivePrefixed = prefixed;
const effectiveSuffix = suffix;

Comment thread
viktorkombov marked this conversation as resolved.
// 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;

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 ?? (indexed.memberName ? `${indexedDisplay}.${indexed.memberName}` : indexedDisplay);
} else if (indexed.status === 'missing' && import.meta.env.DEV) {
console.warn(`[ApiLink] Registry miss for ${type}${member ? `.${member}` : ''}; using legacy URL fallback.`);
}
}
---
{isExcluded
? <code>{displayLabel}</code>
: <a href={url}>{renderCode ? <code>{displayLabel}</code> : displayLabel}</a>}
<a href={url}>{renderCode ? <code>{displayLabel}</code> : displayLabel}</a>
Comment thread
viktorkombov marked this conversation as resolved.
Outdated
Loading