@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
495 lines (421 loc) • 16.2 kB
text/typescript
import { SyntaxKind, ts } from 'ts-morph';
import { detectIndent } from '../../../../../utils';
import { ClassHelper } from './class-helper';
import { IParseDeepIdentifierResult, SymbolHelper } from './symbol-helper';
export class ComponentHelper {
constructor(
private classHelper: ClassHelper,
private symbolHelper: SymbolHelper = new SymbolHelper()
) {}
public getComponentChangeDetection(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'changeDetection', srcFile).pop();
}
public getComponentEncapsulation(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<string> {
return this.symbolHelper.getSymbolDeps(props, 'encapsulation', srcFile);
}
public getComponentPure(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'pure', srcFile).pop();
}
public getComponentName(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'name', srcFile).pop();
}
public getComponentExportAs(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'exportAs', srcFile).pop();
}
public getComponentHostDirectives(
props: ReadonlyArray<ts.ObjectLiteralElementLike>
): Array<any> {
const hostDirectiveSymbolParsed = this.symbolHelper.getSymbolDepsRaw(
props,
'hostDirectives'
);
let hostDirectiveSymbol = null;
if (hostDirectiveSymbolParsed.length > 0) {
hostDirectiveSymbol = hostDirectiveSymbolParsed.pop();
}
const result = [];
if (
hostDirectiveSymbol &&
hostDirectiveSymbol.initializer &&
hostDirectiveSymbol.initializer.elements &&
hostDirectiveSymbol.initializer.elements.length > 0
) {
hostDirectiveSymbol.initializer.elements.forEach(element => {
if (element.kind === SyntaxKind.Identifier) {
result.push({
name: element.escapedText
});
} else if (
element.kind === SyntaxKind.ObjectLiteralExpression &&
element.properties &&
element.properties.length > 0
) {
const parsedDirective: any = {
name: '',
inputs: [],
outputs: []
};
element.properties.forEach(property => {
if (property.name.escapedText === 'directive') {
parsedDirective.name = property.initializer.escapedText;
} else if (property.name.escapedText === 'inputs') {
if (
property.initializer &&
property.initializer.elements &&
property.initializer.elements.length > 0
) {
property.initializer.elements.forEach(propertyElement => {
parsedDirective.inputs.push(propertyElement.text);
});
}
} else if (property.name.escapedText === 'outputs') {
if (
property.initializer &&
property.initializer.elements &&
property.initializer.elements.length > 0
) {
property.initializer.elements.forEach(propertyElement => {
parsedDirective.outputs.push(propertyElement.text);
});
}
}
});
result.push(parsedDirective);
}
});
}
return result;
}
public getComponentHost(
props: ReadonlyArray<ts.ObjectLiteralElementLike>
): Map<string, string> {
return this.getSymbolDepsObject(props, 'host');
}
public getComponentTag(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'tag', srcFile).pop();
}
public getComponentInputsMetadata(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<string> {
return this.symbolHelper.getSymbolDeps(props, 'inputs', srcFile);
}
public getInputOutputSignals(props) {
const inputSignals = [];
const outputSignals = [];
const properties = [];
props.forEach(prop => {
const inputSignal = this.getInputSignal(prop);
if (inputSignal) {
inputSignals.push(inputSignal)
}
const outputSignal = this.getOutputSignal(prop);
if (outputSignal) {
outputSignals.push(outputSignal)
}
if (!inputSignal && !outputSignal) {
properties.push(prop)
}
});
return {inputSignals, outputSignals, properties}
}
public getInputSignal(prop) {
const config =
this.getSignalConfig('input', prop.defaultValue) ??
this.getSignalConfig('model', prop.defaultValue);
if (config) {
return {
...prop,
...config
};
}
return undefined;
}
public getOutputSignal(prop) {
const config =
this.getSignalConfig('output', prop.defaultValue) ??
this.getSignalConfig('model', prop.defaultValue);
if (config) {
return {
...prop,
...config
};
}
return undefined;
}
private getSignalConfig(type: 'input' | 'output' | 'model', defaultValue: string) {
// Matches a quote mark
const quotePattern = `['"\`]`;
// Matches a value for the input
const valuePattern = (capture = true) =>
`(${capture ? '' : '?:'}[^()]*(?:\\([^()]*\\)[^()]*)*)`;
// Matches an optional space
const spacePattern = `(?: )*`;
// Matches the input's type
const typesPattern = `(?:<((?:${valuePattern(false)}(?:${spacePattern}\\|${spacePattern})?)+)>)?`;
// Matches the alias provided in the options
const aliasRegExp = new RegExp(`alias:${spacePattern}${quotePattern}(\\w+)${quotePattern}`);
// Matches a signal of the provided type
const signalRegExp = new RegExp(
`${type}(.required)?${typesPattern}\\(${valuePattern()}?(?:,${spacePattern}({.+}))?\\)`
);
const matches = signalRegExp.exec(defaultValue?.replace(/\n/g, ''));
if (matches) {
const [_match, required, type, defaultValue, options] = matches;
const name = options?.match(aliasRegExp)?.[1];
const result = {
required: !!required,
type: this.parseSignalType(type),
defaultValue
};
if (name) {
return {
...result,
name
};
}
return result;
}
}
public parseSignalType(type: string) {
if (!type) {
return type;
}
// adjust union string expression like: 'foo' | 'bar' | 'test'
// which should be outputed as: "foo" | "bar" | "test"
const unionTypeRegex = /^'([\w-]+)'\s?\|\s?('([\w-]+)'|.*)$/
let typeRest = type;
let newType = "";
let typeMatch: RegExpMatchArray;
while ((typeMatch = typeRest.match(unionTypeRegex))) {
const [, first, rest, second] = typeMatch;
if (second) {
newType += `"${first}" | "${second}"`;
type = newType;
break;
}
newType += `"${first}" | `;
typeRest = rest;
}
return type;
}
public getComponentStandalone(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): boolean {
let result = null;
const parsedData = this.symbolHelper.getSymbolDeps(props, 'standalone', srcFile);
if (parsedData.length === 1) {
result = JSON.parse(parsedData[0]);
}
return result;
}
public getComponentTemplate(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
let t = this.symbolHelper.getSymbolDeps(props, 'template', srcFile, true).pop();
if (t) {
t = detectIndent(t, 0);
t = t.replace(/\n/, '');
t = t.replace(/ +$/gm, '');
}
return t;
}
public getComponentStyleUrls(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string[] {
return this.symbolHelper.getSymbolDeps(props, 'styleUrls', srcFile);
}
public getComponentStyleUrl(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'styleUrl', srcFile).pop();
}
public getComponentShadow(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'shadow', srcFile).pop();
}
public getComponentScoped(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'scoped', srcFile).pop();
}
public getComponentAssetsDir(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile).pop();
}
public getComponentAssetsDirs(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string[] {
return this.sanitizeUrls(this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile));
}
public getComponentStyles(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string[] {
return this.symbolHelper.getSymbolDeps(props, 'styles', srcFile);
}
public getComponentModuleId(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'moduleId', srcFile).pop();
}
public getComponentOutputs(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string[] {
return this.symbolHelper.getSymbolDeps(props, 'outputs', srcFile);
}
public getComponentProviders(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<IParseDeepIdentifierResult> {
return this.symbolHelper
.getSymbolDeps(props, 'providers', srcFile)
.map(name => this.symbolHelper.parseDeepIndentifier(name));
}
public getComponentImports(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<IParseDeepIdentifierResult> {
return this.symbolHelper
.getSymbolDeps(props, 'imports', srcFile)
.map(name => this.symbolHelper.parseDeepIndentifier(name));
}
public getComponentEntryComponents(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<IParseDeepIdentifierResult> {
return this.symbolHelper
.getSymbolDeps(props, 'entryComponents', srcFile)
.map(name => this.symbolHelper.parseDeepIndentifier(name));
}
public getComponentViewProviders(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<IParseDeepIdentifierResult> {
return this.symbolHelper
.getSymbolDeps(props, 'viewProviders', srcFile)
.map(name => this.symbolHelper.parseDeepIndentifier(name));
}
public getComponentTemplateUrl(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): Array<string> {
return this.symbolHelper.getSymbolDeps(props, 'templateUrl', srcFile);
}
public getComponentExampleUrls(text: string): Array<string> | undefined {
let exampleUrlsMatches = text.match(/<example-url>(.*?)<\/example-url>/g);
let exampleUrls = undefined;
if (exampleUrlsMatches && exampleUrlsMatches.length) {
exampleUrls = exampleUrlsMatches.map(function (val) {
return val.replace(/<\/?example-url>/g, '');
});
}
return exampleUrls;
}
public getComponentPreserveWhitespaces(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'preserveWhitespaces', srcFile).pop();
}
public getComponentSelector(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
return this.symbolHelper.getSymbolDeps(props, 'selector', srcFile).pop();
}
private parseProperties(node: ts.ObjectLiteralElementLike): Map<string, string> {
let obj = new Map<string, string>();
const element = node as any;
let properties = element.initializer?.properties || [];
properties.forEach((prop: any) => {
obj.set(prop.name?.text, prop.initializer?.text);
});
return obj;
}
public getSymbolDepsObject(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
type: string,
multiLine?: boolean
): Map<string, string> {
let i = 0,
len = props.length,
filteredProps = [];
for (i; i < len; i++) {
if (props[i].name && (props[i].name as any).text === type) {
filteredProps.push(props[i]);
}
}
return filteredProps.map(x => this.parseProperties(x)).pop();
}
public getComponentIO(
filename: string,
sourceFile: ts.SourceFile,
node: ts.Node,
fileBody,
astFile: ts.SourceFile
): any {
/**
* Copyright https://github.com/ng-bootstrap/ng-bootstrap
*/
let reducedSource = fileBody ? fileBody.statements : sourceFile.statements;
let res = reducedSource.reduce((directive, statement) => {
if (ts.isClassDeclaration(statement)) {
if (statement.pos === node.pos && statement.end === node.end) {
return directive.concat(
this.classHelper.visitClassDeclaration(
filename,
statement,
sourceFile,
astFile
)
);
}
}
return directive;
}, []);
return res[0] || {};
}
private sanitizeUrls(urls: Array<string>): Array<string> {
return urls.map(url => url.replace('./', ''));
}
}
export class ComponentCache {
private cache: Map<string, any> = new Map();
public get(key: string): any {
return this.cache.get(key);
}
public set(key: string, value: any): void {
this.cache.set(key, value);
}
}