mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): Improves symbol linking for Angular Aria selectors
Improves the symbol linking logic to handle Angular component selectors (e.g., ngCombobox). It attempts to convert Angular selector patterns to their corresponding class names, improving navigation to Angular API documentation.
(cherry picked from commit 2d854e01bc)
This commit is contained in:
parent
bc6cb6830e
commit
b7ccb308b8
20 changed files with 159 additions and 18 deletions
|
|
@ -11,6 +11,7 @@ import type {DocEntry, EntryCollection, FunctionEntry, JsDocTagEntry} from '@ang
|
|||
|
||||
export interface ManifestEntry {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
type: string;
|
||||
category: string | undefined;
|
||||
deprecated: {version: string | undefined} | undefined;
|
||||
|
|
@ -81,6 +82,7 @@ export function generateManifest(apiCollections: EntryCollection[]): Manifest {
|
|||
.filter((entry) => !isHiddenEntry(entry))
|
||||
.map((entry: DocEntry) => ({
|
||||
name: entry.name,
|
||||
...(entry.aliases && {aliases: entry.aliases}),
|
||||
type: entry.entryType,
|
||||
deprecated: getTagSinceVersion(entry, 'deprecated', true),
|
||||
developerPreview: getTagSinceVersion(entry, 'developerPreview'),
|
||||
|
|
|
|||
|
|
@ -21,8 +21,15 @@ export function setCurrentSymbol(symbol: string): void {
|
|||
currentSymbol = symbol;
|
||||
}
|
||||
|
||||
export function getSymbols() {
|
||||
return symbols;
|
||||
/** Convert Record<string, string> to ApiEntries format */
|
||||
export function getSymbolsAsApiEntries(): Record<string, {moduleName: string}> {
|
||||
const result: Record<string, {moduleName: string}> = {};
|
||||
|
||||
for (const symbol in symbols) {
|
||||
result[symbol] = {moduleName: symbols[symbol]};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getCurrentSymbol(): string | undefined {
|
||||
|
|
@ -34,7 +41,7 @@ export function setSymbols(newSymbols: Record<string, string>): void {
|
|||
}
|
||||
|
||||
export function getSymbolUrl(symbol: string): string | undefined {
|
||||
return sharedGetSymbolUrl(symbol, symbols);
|
||||
return sharedGetSymbolUrl(symbol, getSymbolsAsApiEntries());
|
||||
}
|
||||
|
||||
export function unknownSymbolMessage(link: string, symbol: string): string {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {parseMarkdown} from '../../../shared/marked/parse.mjs';
|
|||
import {getHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
import {
|
||||
getCurrentSymbol,
|
||||
getSymbols,
|
||||
getSymbolsAsApiEntries,
|
||||
getSymbolUrl,
|
||||
unknownSymbolMessage,
|
||||
} from '../symbol-context.mjs';
|
||||
|
|
@ -107,7 +107,7 @@ export function addHtmlUsageNotes<T extends HasJsDocTags>(entry: T): T & HasHtml
|
|||
function getHtmlForJsDocText(text: string): string {
|
||||
const mdToParse = convertLinks(wrapExampleHtmlElementsWithCode(text));
|
||||
const parsed = parseMarkdown(mdToParse, {
|
||||
apiEntries: getSymbols(),
|
||||
apiEntries: getSymbolsAsApiEntries(),
|
||||
highlighter: getHighlighterInstance(),
|
||||
});
|
||||
return addApiLinksToHtml(parsed);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {hasUnknownAnchors} from './helpers.mjs';
|
|||
type ApiManifest = ApiManifestPackage[];
|
||||
interface ApiManifestPackage {
|
||||
moduleName: string;
|
||||
entries: {name: string}[];
|
||||
entries: {name: string; aliases?: string[]}[];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
|
@ -66,10 +66,12 @@ async function main() {
|
|||
|
||||
main();
|
||||
|
||||
function mapManifestToEntries(apiManifest: ApiManifest): Record<string, string> {
|
||||
function mapManifestToEntries(
|
||||
apiManifest: ApiManifest,
|
||||
): Record<string, {moduleName: string; targetSymbol?: string}> {
|
||||
const duplicateEntries = new Set<string>();
|
||||
|
||||
const entryToModuleMap: Record<string, string> = {};
|
||||
const entryToModuleMap: Record<string, {moduleName: string; targetSymbol?: string}> = {};
|
||||
for (const pkg of apiManifest) {
|
||||
for (const entry of pkg.entries) {
|
||||
if (duplicateEntries.has(entry.name)) {
|
||||
|
|
@ -78,7 +80,19 @@ function mapManifestToEntries(apiManifest: ApiManifest): Record<string, string>
|
|||
delete entryToModuleMap[entry.name];
|
||||
duplicateEntries.add(entry.name);
|
||||
} else {
|
||||
entryToModuleMap[entry.name] = pkg.moduleName.replace(/^@angular\//, '');
|
||||
const normalizedModuleName = pkg.moduleName.replace(/^@angular\//, '');
|
||||
|
||||
entryToModuleMap[entry.name] = {moduleName: normalizedModuleName};
|
||||
|
||||
// If there are aliases, create entries for each alias
|
||||
if (entry.aliases) {
|
||||
for (const alias of entry.aliases) {
|
||||
entryToModuleMap[alias] = {
|
||||
moduleName: normalizedModuleName,
|
||||
targetSymbol: entry.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project", "zoneless_jasmine_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
|
@ -20,3 +20,17 @@ ts_project(
|
|||
"//adev:node_modules/shiki",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "linking_test_lib",
|
||||
testonly = True,
|
||||
srcs = ["test/linking.spec.mts"],
|
||||
deps = [
|
||||
":linking",
|
||||
],
|
||||
)
|
||||
|
||||
zoneless_jasmine_test(
|
||||
name = "linking_test",
|
||||
data = [":linking_test_lib"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export function shouldLinkSymbol(symbol: string): boolean {
|
|||
return !LINK_EXEMPT.has(symbol);
|
||||
}
|
||||
|
||||
export type ApiEntries = Record<string, string>; // symbolName -> moduleName (without @angular/ prefix)
|
||||
// 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.
|
||||
|
|
@ -53,9 +54,15 @@ export function getSymbolUrl(symbol: string, apiEntries: ApiEntries): string | u
|
|||
|
||||
const {symbolName, propName} = extractFromSymbol(symbol);
|
||||
// We don't want to match entries like "constructor"
|
||||
const apiEntry = Object.hasOwn(apiEntries, symbolName) && apiEntries[symbolName];
|
||||
if (!Object.hasOwn(apiEntries, symbolName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return apiEntry ? `/api/${apiEntry}/${symbolName}${propName ? `#${propName}` : ''}` : undefined;
|
||||
const apiEntry = apiEntries[symbolName];
|
||||
const moduleName = apiEntry.moduleName;
|
||||
const targetSymbol = apiEntry.targetSymbol ?? symbolName;
|
||||
|
||||
return `/api/${moduleName}/${targetSymbol}${propName ? `#${propName}` : ''}`;
|
||||
}
|
||||
|
||||
function hasMoreThanOneDot(str: string) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {HighlighterGeneric} from 'shiki';
|
|||
|
||||
export interface RendererContext {
|
||||
markdownFilePath?: string;
|
||||
apiEntries?: Record<string, string>;
|
||||
apiEntries?: Record<string, {moduleName: string; aliases?: string[]}>;
|
||||
highlighter: HighlighterGeneric<any, any>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ export const rendererContext: RendererContext = {
|
|||
markdownFilePath: '',
|
||||
highlighter: null!,
|
||||
apiEntries: {
|
||||
CommonModule: 'angular/common',
|
||||
bootstrapApplication: 'angular/platform-browser',
|
||||
ApplicationRef: 'angular/core',
|
||||
Router: 'angular/router',
|
||||
CommonModule: {moduleName: 'angular/common'},
|
||||
bootstrapApplication: {moduleName: 'angular/platform-browser'},
|
||||
ApplicationRef: {moduleName: 'angular/core'},
|
||||
Router: {moduleName: 'angular/router'},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
65
adev/shared-docs/pipeline/shared/test/linking.spec.mts
Normal file
65
adev/shared-docs/pipeline/shared/test/linking.spec.mts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {getSymbolUrl} from '../linking.mjs';
|
||||
|
||||
describe('getSymbolUrl', () => {
|
||||
it('should resolve class names to API URLs', () => {
|
||||
const apiEntries = {
|
||||
Combobox: {moduleName: 'aria/combobox'},
|
||||
AccordionPanel: {moduleName: 'aria/accordion'},
|
||||
Grid: {moduleName: 'aria/grid'},
|
||||
};
|
||||
|
||||
expect(getSymbolUrl('Combobox', apiEntries)).toBe('/api/aria/combobox/Combobox');
|
||||
expect(getSymbolUrl('AccordionPanel', apiEntries)).toBe('/api/aria/accordion/AccordionPanel');
|
||||
expect(getSymbolUrl('Grid', apiEntries)).toBe('/api/aria/grid/Grid');
|
||||
});
|
||||
|
||||
it('should resolve selector aliases to class names', () => {
|
||||
const apiEntries = {
|
||||
Combobox: {moduleName: 'aria/combobox'},
|
||||
ngCombobox: {moduleName: 'aria/combobox', targetSymbol: 'Combobox'},
|
||||
AccordionPanel: {moduleName: 'aria/accordion'},
|
||||
ngAccordionPanel: {moduleName: 'aria/accordion', targetSymbol: 'AccordionPanel'},
|
||||
GridCell: {moduleName: 'aria/grid'},
|
||||
ngGridCell: {moduleName: 'aria/grid', targetSymbol: 'GridCell'},
|
||||
};
|
||||
|
||||
expect(getSymbolUrl('ngCombobox', apiEntries)).toBe('/api/aria/combobox/Combobox');
|
||||
expect(getSymbolUrl('ngAccordionPanel', apiEntries)).toBe('/api/aria/accordion/AccordionPanel');
|
||||
expect(getSymbolUrl('ngGridCell', apiEntries)).toBe('/api/aria/grid/GridCell');
|
||||
});
|
||||
|
||||
it('should handle selector aliases with properties', () => {
|
||||
const apiEntries = {
|
||||
AccordionPanel: {moduleName: 'aria/accordion'},
|
||||
ngAccordionPanel: {moduleName: 'aria/accordion', targetSymbol: 'AccordionPanel'},
|
||||
ComboboxInput: {moduleName: 'aria/combobox'},
|
||||
ngComboboxInput: {moduleName: 'aria/combobox', targetSymbol: 'ComboboxInput'},
|
||||
};
|
||||
|
||||
expect(getSymbolUrl('ngAccordionPanel.visible', apiEntries)).toBe(
|
||||
'/api/aria/accordion/AccordionPanel#visible',
|
||||
);
|
||||
expect(getSymbolUrl('ngComboboxInput.value', apiEntries)).toBe(
|
||||
'/api/aria/combobox/ComboboxInput#value',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown symbols', () => {
|
||||
const apiEntries = {
|
||||
AccordionPanel: {moduleName: 'aria/accordion'},
|
||||
ngAccordionPanel: {moduleName: 'aria/accordion', targetSymbol: 'AccordionPanel'},
|
||||
};
|
||||
|
||||
expect(getSymbolUrl('UnknownSymbol', apiEntries)).toBeUndefined();
|
||||
expect(getSymbolUrl('unknownSelector', apiEntries)).toBeUndefined();
|
||||
expect(getSymbolUrl('ngUnknownDirective', apiEntries)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import {ApiItemType} from './api-item-type';
|
|||
|
||||
export interface ApiManifestEntry {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
type: ApiItemType;
|
||||
category: string | undefined;
|
||||
deprecated: {version: string | undefined} | undefined;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "AccordionPanel",
|
||||
"aliases": ["ngAccordionPanel"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
|
|
@ -157,6 +158,7 @@
|
|||
},
|
||||
{
|
||||
"name": "AccordionTrigger",
|
||||
"aliases": ["ngAccordionTrigger"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -357,6 +359,7 @@
|
|||
},
|
||||
{
|
||||
"name": "AccordionGroup",
|
||||
"aliases": ["ngAccordionGroup"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -522,6 +525,7 @@
|
|||
},
|
||||
{
|
||||
"name": "AccordionContent",
|
||||
"aliases": ["ngAccordionContent"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "Combobox",
|
||||
"aliases": ["ngCombobox"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
|
|
@ -200,6 +201,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ComboboxInput",
|
||||
"aliases": ["ngComboboxInput"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -261,6 +263,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ComboboxPopupContainer",
|
||||
"aliases": ["ngComboboxPopupContainer"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [],
|
||||
|
|
@ -282,6 +285,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ComboboxPopup",
|
||||
"aliases": ["ngComboboxPopup"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -323,6 +327,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ComboboxDialog",
|
||||
"aliases": ["ngComboboxDialog"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "Grid",
|
||||
"aliases" : ["ngGrid"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
},
|
||||
{
|
||||
"name": "GridRow",
|
||||
"aliases" : ["ngGridRow"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -228,6 +230,7 @@
|
|||
},
|
||||
{
|
||||
"name": "GridCell",
|
||||
"aliases" : ["ngGridCell"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -443,6 +446,7 @@
|
|||
},
|
||||
{
|
||||
"name": "GridCellWidget",
|
||||
"aliases" : ["ngGridCellWidget"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "Listbox",
|
||||
"aliases" : ["ngListbox"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
|
|
@ -238,6 +239,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Option",
|
||||
"aliases" : ["ngOption"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "MenuTrigger",
|
||||
"aliases" : ["ngMenuTrigger"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -181,6 +182,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Menu",
|
||||
"aliases" : ["ngMenu"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
|
|
@ -348,6 +350,7 @@
|
|||
},
|
||||
{
|
||||
"name": "MenuBar",
|
||||
"aliases" : ["ngMenuBar"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -510,6 +513,7 @@
|
|||
},
|
||||
{
|
||||
"name": "MenuItem",
|
||||
"aliases" : ["ngMenuItem"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -724,6 +728,7 @@
|
|||
},
|
||||
{
|
||||
"name": "MenuContent",
|
||||
"aliases" : ["ngMenuContent"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "Tabs",
|
||||
"aliases" : ["ngTabs"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TabList",
|
||||
"aliases" : ["ngTabList"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -301,6 +303,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Tab",
|
||||
"aliases" : ["ngTab"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -500,6 +503,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TabPanel",
|
||||
"aliases" : ["ngTabPanel"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
|
|
@ -631,6 +635,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TabContent",
|
||||
"aliases" : ["ngTabContent"],
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"entries": [
|
||||
{
|
||||
"name": "Toolbar",
|
||||
"aliases" : ["ngToolbar"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ToolbarWidget",
|
||||
"aliases" : ["ngToolbarWidget"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
@ -302,6 +304,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ToolbarWidgetGroup",
|
||||
"aliases" : ["ngToolbarWidgetGroup"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TreeItem",
|
||||
"aliases" : ["ngTreeItem"],
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ generate_guides(
|
|||
srcs = glob([
|
||||
"*.md",
|
||||
]),
|
||||
api_manifest = "//adev/src/assets:docs_api_manifest",
|
||||
data = [
|
||||
"//adev/src/content/examples",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface DocEntryWithSourceInfo extends DocEntry {
|
|||
/** Base type for all documentation entities. */
|
||||
export interface DocEntry {
|
||||
entryType: EntryType;
|
||||
aliases?: string[];
|
||||
name: string;
|
||||
description: string;
|
||||
rawComment: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue