🌐 AI搜索 & 代理 主页
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
60 changes: 58 additions & 2 deletions packages/compiler-cli/src/ngtsc/docs/src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DocEntryWithSourceInfo,
EntryType,
MemberType,
type DirectiveEntry,
type InterfaceEntry,
type MemberEntry,
type NamespaceEntry,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -125,6 +126,11 @@ export class DocsExtractor {
ts.getLineAndCharacterOfPosition(realSourceFile, declarations[0].getEnd()).line + 1,
};

const aliases = getAliasesFromEntry(entry);
if (aliases.length > 0) {
entry.aliases = aliases;
}

// The exported name of an API may be different from its declaration name, so
// use the declaration name.
entries.push({...entry, name: exportName});
Expand All @@ -139,7 +145,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) {
Expand Down Expand Up @@ -299,3 +305,53 @@ function isNamespaceEntry(e: DocEntry | null): e is NamespaceEntry {
function isInterfaceEntry(e: DocEntry | null): e is InterfaceEntry {
return e?.entryType === EntryType.Interface;
}

/**
* 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;
}

/**
* Extracts alias names from selector (for directives/components).
*/
function getAliasesFromEntry(entry: DocEntry): string[] {
if (isDirectiveEntry(entry)) {
return extractAliasesFromSelector(entry.selector);
}

return [];
}

function isDirectiveEntry(entry: DocEntry): entry is DirectiveEntry {
return entry.entryType === EntryType.Directive || entry.entryType === EntryType.Component;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
24 changes: 2 additions & 22 deletions packages/compiler-cli/src/ngtsc/docs/src/properties_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});