mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(docs-infra): Extracts aliases from doc entries
Implements a mechanism to extract alias names from a directive's selector.
This commit is contained in:
committed by
Kirill Cherkashin
parent
b735c45974
commit
ed7bfb3c88
@@ -92,12 +92,16 @@ class DirectiveExtractor extends ClassExtractor {
|
||||
|
||||
/** Extract docs info for directives and components (including underlying class info). */
|
||||
override extract(): DirectiveEntry {
|
||||
const selector = this.metadata.selector ?? '';
|
||||
const aliases = extractAliasesFromSelector(selector);
|
||||
|
||||
return {
|
||||
...super.extract(),
|
||||
isStandalone: this.metadata.isStandalone,
|
||||
selector: this.metadata.selector ?? '',
|
||||
selector,
|
||||
exportAs: this.metadata.exportAs ?? [],
|
||||
entryType: this.metadata.isComponent ? EntryType.Component : EntryType.Directive,
|
||||
...(aliases.length > 0 && {aliases}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,3 +226,38 @@ function extractPipeSyntax(metadata: PipeMeta, classDeclaration: ts.ClassDeclara
|
||||
|
||||
return `{{ value_expression | ${metadata.name}${paramNames.length ? ':' + paramNames.join(':') : ''} }}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts aliases from a selector string.
|
||||
*
|
||||
* Parses selectors like:
|
||||
* - `[ngTabs]` => `['ngTabs']`
|
||||
* - `input[ngComboboxInput]` => `['ngComboboxInput']`
|
||||
* - `ng-template[ngComboboxPopupContainer]` => `['ngComboboxPopupContainer']`
|
||||
* - `[attr1][attr2]` => `['attr1', 'attr2']`
|
||||
* - `.class-name` => `[]` (classes are not extracted)
|
||||
*
|
||||
* @param selector The CSS selector string from directive/component metadata
|
||||
* @returns Array of attribute names that can be used as aliases
|
||||
*/
|
||||
function extractAliasesFromSelector(selector: string): string[] {
|
||||
if (!selector) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const aliases: string[] = [];
|
||||
// Match attribute selectors: [attributeName] or element[attributeName]
|
||||
// This regex captures the attribute name inside square brackets
|
||||
const attributeRegex = /\[([^\]=]+)(?:=[^\]]+)?\]/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = attributeRegex.exec(selector)) !== null) {
|
||||
const attributeName = match[1].trim();
|
||||
// Skip empty attributes
|
||||
if (attributeName) {
|
||||
aliases.push(attributeName);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DocEntryWithSourceInfo,
|
||||
EntryType,
|
||||
MemberType,
|
||||
type DirectiveEntry,
|
||||
type InterfaceEntry,
|
||||
type MemberEntry,
|
||||
type NamespaceEntry,
|
||||
@@ -82,7 +83,7 @@ export class DocsExtractor {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = this.extractDeclarations(exportName, declarations);
|
||||
const entry = this.extractDeclarations(declarations);
|
||||
if (entry && !isIgnoredDocEntry(entry)) {
|
||||
// The source file parameter is the package entry: the index.ts
|
||||
// We want the real source file of the declaration.
|
||||
@@ -139,7 +140,7 @@ export class DocsExtractor {
|
||||
* the same name. This is used to combine entries, e.g. for a type and a namespace that are
|
||||
* exported under the same name.
|
||||
*/
|
||||
private extractDeclarations(exportName: string, nodes: ts.Declaration[]): DocEntry | null {
|
||||
private extractDeclarations(nodes: ts.Declaration[]): DocEntry | null {
|
||||
const entries = nodes.map((node) => this.extractDeclaration(node));
|
||||
const decorator = entries.find((e) => e?.entryType === EntryType.Decorator);
|
||||
if (decorator) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
|
||||
import {ClassEntry, EntryType, InterfaceEntry} from './entities';
|
||||
import {EntryType, InterfaceEntry} from './entities';
|
||||
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
|
||||
import {PropertiesExtractor} from './properties_extractor';
|
||||
|
||||
|
||||
@@ -8,34 +8,14 @@
|
||||
|
||||
import ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {
|
||||
DirectiveMeta,
|
||||
InputMapping,
|
||||
InputOrOutput,
|
||||
MetadataReader,
|
||||
NgModuleMeta,
|
||||
PipeMeta,
|
||||
} from '../../metadata';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
|
||||
import {
|
||||
ClassEntry,
|
||||
DirectiveEntry,
|
||||
EntryType,
|
||||
InterfaceEntry,
|
||||
MemberEntry,
|
||||
MemberTags,
|
||||
MemberType,
|
||||
MethodEntry,
|
||||
PipeEntry,
|
||||
PropertyEntry,
|
||||
} from './entities';
|
||||
import {MemberEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from './entities';
|
||||
import {isAngularPrivateName} from './filters';
|
||||
import {FunctionExtractor} from './function_extractor';
|
||||
import {extractGenerics} from './generics_extractor';
|
||||
import {isInternal} from './internal';
|
||||
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
|
||||
import {extractJsDocDescription, extractJsDocTags} from './jsdoc_extractor';
|
||||
import {extractResolvedTypeString} from './type_extractor';
|
||||
|
||||
// For the purpose of extraction, we can largely treat properties and accessors the same.
|
||||
|
||||
@@ -282,5 +282,161 @@ runInEachFileSystem(() => {
|
||||
expect(isAdminSetter.name).toBe('isAdmin');
|
||||
expect(isAdminSetter.memberTags).toContain(MemberTags.Input);
|
||||
});
|
||||
|
||||
describe('selector alias extraction', () => {
|
||||
it('should extract alias from simple attribute selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: '[ngTabs]',
|
||||
})
|
||||
export class NgTabs { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toEqual(['ngTabs']);
|
||||
});
|
||||
|
||||
it('should extract alias from element with attribute selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: 'input[ngComboboxInput]',
|
||||
})
|
||||
export class NgComboboxInput { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toEqual(['ngComboboxInput']);
|
||||
});
|
||||
|
||||
it('should extract alias from ng-template with attribute selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: 'ng-template[ngComboboxPopupContainer]',
|
||||
})
|
||||
export class NgComboboxPopupContainer { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toEqual(['ngComboboxPopupContainer']);
|
||||
});
|
||||
|
||||
it('should extract multiple aliases from multiple attribute selectors', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: '[attr1],[attr2]',
|
||||
})
|
||||
export class MultiAttr { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toEqual(['attr1', 'attr2']);
|
||||
});
|
||||
|
||||
it('should extract aliases from complex selector with element and multiple attributes', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: 'button[cdkButton]',
|
||||
})
|
||||
export class CdkButton { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toEqual(['cdkButton']);
|
||||
});
|
||||
|
||||
it('should not extract aliases from element-only selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '',
|
||||
})
|
||||
export class AppRoot { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const componentEntry = docs[0];
|
||||
expect(componentEntry.aliases).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not extract aliases from class selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: '.my-class',
|
||||
template: '',
|
||||
})
|
||||
export class MyClass { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const componentEntry = docs[0];
|
||||
expect(componentEntry.aliases).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty selector', () => {
|
||||
env.write(
|
||||
'index.ts',
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
@Directive({
|
||||
selector: '',
|
||||
})
|
||||
export class EmptySelector { }
|
||||
`,
|
||||
);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const directiveEntry = docs[0];
|
||||
expect(directiveEntry.aliases).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user