mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs: Enable direct links to class/interface methods for @link (#57615)
PR Close #57615
This commit is contained in:
parent
bf5f2e73d3
commit
dbdd7875dd
6 changed files with 140 additions and 23 deletions
|
|
@ -7,6 +7,8 @@ import {
|
|||
createCompilerHost,
|
||||
DocEntry,
|
||||
EntryCollection,
|
||||
InterfaceEntry,
|
||||
ClassEntry,
|
||||
} from '@angular/compiler-cli';
|
||||
import ts from 'typescript';
|
||||
|
||||
|
|
@ -81,10 +83,28 @@ function main() {
|
|||
|
||||
// Exported symbols from the current package
|
||||
...apiDoc.entries.map((entry) => [entry.name, moduleName]),
|
||||
|
||||
// Also doing it for every member of classes/interfaces
|
||||
...apiDoc.entries.flatMap((entry) => [
|
||||
[entry.name, moduleName],
|
||||
...getEntriesFromMembers(entry).map((member) => [member, moduleName]),
|
||||
]),
|
||||
],
|
||||
} as EntryCollection);
|
||||
|
||||
writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
|
||||
}
|
||||
|
||||
function getEntriesFromMembers(entry: DocEntry): string[] {
|
||||
if (!hasMembers(entry)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return entry.members.map((member) => `${entry.name}.${member.name}`);
|
||||
}
|
||||
|
||||
function hasMembers(entry: DocEntry): entry is InterfaceEntry | ClassEntry {
|
||||
return 'members' in entry;
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {configureMarkedGlobally} from './marked/configuration';
|
|||
import {getRenderable} from './processing';
|
||||
import {renderEntry} from './rendering';
|
||||
import {initHighlighter} from './shiki/shiki';
|
||||
import {setSymbols} from './symbol-context';
|
||||
import {setCurrentSymbol, setSymbols} from './symbol-context';
|
||||
|
||||
/** The JSON data file format for extracted API reference info. */
|
||||
interface EntryCollection {
|
||||
|
|
@ -110,9 +110,10 @@ async function main() {
|
|||
// Setting the symbols are a global context for the rendering templates of this entry
|
||||
setSymbols(collection.symbols);
|
||||
|
||||
const renderableEntries = extractedEntries.map((entry) =>
|
||||
getRenderable(entry, collection.moduleName),
|
||||
);
|
||||
const renderableEntries = extractedEntries.map((entry) => {
|
||||
setCurrentSymbol(entry.name);
|
||||
return getRenderable(entry, collection.moduleName);
|
||||
});
|
||||
|
||||
const htmlOutputs = renderableEntries.map(renderEntry);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,11 @@ import {EnumReference} from './templates/enum-reference';
|
|||
import {FunctionReference} from './templates/function-reference';
|
||||
import {InitializerApiFunction} from './templates/initializer-api-function';
|
||||
import {TypeAliasReference} from './templates/type-alias-reference';
|
||||
import {setCurrentSymbol} from './symbol-context';
|
||||
|
||||
/** Given a doc entry, get the transformed version of the entry for rendering. */
|
||||
export function renderEntry(renderable: DocEntryRenderable | CliCommandRenderable): string {
|
||||
setCurrentSymbol(renderable.name);
|
||||
if (isCliEntry(renderable)) {
|
||||
return render(CliCommandReference(renderable));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
|
||||
let symbols = new Map<string, string>();
|
||||
|
||||
// This is used to store the currently processed symbol (usually a class or an interface)
|
||||
let currentSymbol: string | undefined;
|
||||
|
||||
export function setSymbols(newSymbols: Map<string, string>): void {
|
||||
symbols = newSymbols;
|
||||
}
|
||||
|
|
@ -12,7 +15,23 @@ export function setSymbols(newSymbols: Map<string, string>): void {
|
|||
/**
|
||||
* Returns the module name of a symbol.
|
||||
* eg: 'ApplicationRef' => 'core', 'FormControl' => 'forms'
|
||||
* Also supports class.member, 'NgZone.runOutsideAngular => 'core'
|
||||
*/
|
||||
export function getModuleName(symbol: string): string | undefined {
|
||||
return symbols.get(symbol)?.replace('@angular/', '');
|
||||
const moduleName = symbols.get(symbol);
|
||||
return moduleName?.replace('@angular/', '');
|
||||
}
|
||||
|
||||
export function setCurrentSymbol(symbol: string): void {
|
||||
currentSymbol = symbol;
|
||||
}
|
||||
|
||||
export function getCurrentSymbol(): string | undefined {
|
||||
return currentSymbol;
|
||||
}
|
||||
|
||||
export function logUnknownSymbol(link: string, symbol: string): void {
|
||||
console.warn(
|
||||
`WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
import {setSymbols} from '../../symbol-context';
|
||||
import {setCurrentSymbol, setSymbols} from '../../symbol-context';
|
||||
import {addHtmlAdditionalLinks} from '../../transforms/jsdoc-transforms';
|
||||
|
||||
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
|
||||
describe('jsdoc transforms', () => {
|
||||
it('should transform links', () => {
|
||||
setSymbols(new Map([['Route', 'test']]));
|
||||
setCurrentSymbol('Router');
|
||||
setSymbols(
|
||||
new Map([
|
||||
['Route', 'test'],
|
||||
['Router', 'test'],
|
||||
['Router.someMethod', 'test'],
|
||||
['Router.someMethodWithParenthesis', 'test'],
|
||||
['FormGroup', 'test'],
|
||||
['FormGroup.someMethod', 'test'],
|
||||
]),
|
||||
);
|
||||
|
||||
const entry = addHtmlAdditionalLinks({
|
||||
jsdocTags: [
|
||||
{
|
||||
|
|
@ -23,6 +34,30 @@ describe('jsdoc transforms', () => {
|
|||
name: 'see',
|
||||
comment: '{@link Route Something else}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link #someMethod}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link #someMethodWithParenthesis()}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link someMethod()}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link FormGroup.someMethod()}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link https://angular.dev/api/core/ApplicationRef}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link https://angular.dev}',
|
||||
},
|
||||
],
|
||||
moduleName: 'test',
|
||||
});
|
||||
|
|
@ -48,5 +83,31 @@ describe('jsdoc transforms', () => {
|
|||
label: 'Something else',
|
||||
url: '/api/test/Route',
|
||||
});
|
||||
|
||||
expect(entry.additionalLinks[4]).toEqual({
|
||||
label: 'someMethod',
|
||||
url: '/api/test/Router#someMethod',
|
||||
});
|
||||
expect(entry.additionalLinks[5]).toEqual({
|
||||
label: 'someMethodWithParenthesis()',
|
||||
url: '/api/test/Router#someMethodWithParenthesis',
|
||||
});
|
||||
expect(entry.additionalLinks[6]).toEqual({
|
||||
label: 'someMethod()',
|
||||
url: '/api/test/Router#someMethod',
|
||||
});
|
||||
expect(entry.additionalLinks[7]).toEqual({
|
||||
label: 'FormGroup.someMethod()',
|
||||
url: '/api/test/FormGroup#someMethod',
|
||||
});
|
||||
|
||||
expect(entry.additionalLinks[8]).toEqual({
|
||||
label: 'ApplicationRef',
|
||||
url: 'https://angular.dev/api/core/ApplicationRef',
|
||||
});
|
||||
expect(entry.additionalLinks[9]).toEqual({
|
||||
label: 'angular.dev',
|
||||
url: 'https://angular.dev',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
|
||||
import {getLinkToModule} from './url-transforms';
|
||||
import {addApiLinksToHtml} from './code-transforms';
|
||||
import {getModuleName} from '../symbol-context';
|
||||
import {getCurrentSymbol, getModuleName, logUnknownSymbol} from '../symbol-context';
|
||||
|
||||
export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes';
|
||||
export const JS_DOC_SEE_TAG = 'see';
|
||||
|
|
@ -197,27 +197,41 @@ function convertLinks(text: string) {
|
|||
});
|
||||
}
|
||||
|
||||
function parseAtLink(link: string) {
|
||||
function parseAtLink(link: string): {label: string; url: string} {
|
||||
// Because of microsoft/TypeScript/issues/59679
|
||||
// getTextOfJSDocComment introduces an extra space between the symbol and a trailing ()
|
||||
link = link.replace(/ \(\)$/, '');
|
||||
|
||||
let [rawSymbol, description] = link.split(/\s(.+)/);
|
||||
let [symbol, subSymbol] = rawSymbol.split(/(?:#|\.)/);
|
||||
|
||||
const moduleName = getModuleName(symbol)!;
|
||||
if (!moduleName) {
|
||||
logWarning(link, symbol);
|
||||
if (rawSymbol.startsWith('#')) {
|
||||
rawSymbol = rawSymbol.substring(1);
|
||||
} else if (rawSymbol.startsWith('http://') || rawSymbol.startsWith('https://')) {
|
||||
return {
|
||||
url: rawSymbol,
|
||||
label: rawSymbol.split('/').pop()!,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: description ?? rawSymbol,
|
||||
url: getLinkToModule(moduleName, symbol, subSymbol),
|
||||
};
|
||||
}
|
||||
let [symbol, subSymbol] = rawSymbol.replace(/\(\)$/, '').split(/(?:#|\.)/);
|
||||
|
||||
function logWarning(link: string, symbol: string) {
|
||||
// TODO: remove the links that generate this error
|
||||
// TODO: throw an error when there are no more warning generated
|
||||
console.warn(`WARNING: {@link ${link}} is invalid, ${symbol} is unknown in this context`);
|
||||
let moduleName = getModuleName(symbol);
|
||||
const label = description ?? rawSymbol;
|
||||
|
||||
const currentSymbol = getCurrentSymbol();
|
||||
|
||||
if (!moduleName) {
|
||||
// 2nd attemp, try to get the module name in the context of the current symbol
|
||||
moduleName = getModuleName(`${currentSymbol}.${symbol}`);
|
||||
|
||||
if (!moduleName || !currentSymbol) {
|
||||
// TODO: remove the links that generate this error
|
||||
// TODO: throw an error when there are no more warning generated
|
||||
logUnknownSymbol(link, symbol);
|
||||
return {label, url: '#'};
|
||||
}
|
||||
subSymbol = symbol;
|
||||
symbol = currentSymbol;
|
||||
}
|
||||
|
||||
return {label, url: getLinkToModule(moduleName, symbol, subSymbol)};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue