docs(docs-infra): Add dedicated support for decorators. (#57595)

PR Close #57595
This commit is contained in:
Matthieu Riegler
2024-08-21 01:46:30 +02:00
committed by Andrew Scott
parent 88457ab9be
commit 07a485fdba
8 changed files with 97 additions and 20 deletions
@@ -1,6 +1,7 @@
import {
ClassEntry,
ConstantEntry,
DecoratorEntry,
DocEntry,
EntryType,
EnumEntry,
@@ -20,6 +21,7 @@ import {
ClassEntryRenderable,
CliCommandRenderable,
ConstantEntryRenderable,
DecoratorEntryRenderable,
DocEntryRenderable,
EnumEntryRenderable,
FunctionEntryRenderable,
@@ -42,11 +44,16 @@ export function isClassEntry(entry: DocEntry): entry is ClassEntry {
entry.entryType === EntryType.Component ||
entry.entryType === EntryType.Pipe ||
entry.entryType === EntryType.NgModule ||
entry.entryType === EntryType.Directive ||
entry.entryType === EntryType.Decorator
entry.entryType === EntryType.Directive
);
}
export function isDecoratorEntry(entry: DocEntryRenderable): entry is DecoratorEntryRenderable;
export function isDecoratorEntry(entry: DocEntry): entry is DecoratorEntry;
export function isDecoratorEntry(entry: DocEntry): entry is DecoratorEntry {
return entry.entryType === EntryType.Decorator;
}
/** Gets whether the given entry represents a constant */
export function isConstantEntry(entry: DocEntryRenderable): entry is ConstantEntryRenderable;
export function isConstantEntry(entry: DocEntry): entry is ConstantEntry;
@@ -9,6 +9,7 @@
import {
ClassEntry,
ConstantEntry,
DecoratorEntry,
DocEntry,
EnumEntry,
FunctionEntry,
@@ -59,6 +60,12 @@ export type ClassEntryRenderable = ClassEntry &
members: MemberEntryRenderable[];
};
export type DecoratorEntryRenderable = DecoratorEntry &
DocEntryRenderable &
HasRenderableToc & {
members: MemberEntryRenderable[];
};
/** Documentation entity for a TypeScript enum augmented transformed content for rendering. */
export type EnumEntryRenderable = EnumEntry &
DocEntryRenderable &
@@ -12,6 +12,7 @@ import {
isClassEntry,
isCliEntry,
isConstantEntry,
isDecoratorEntry,
isEnumEntry,
isFunctionEntry,
isInitializerApiFunctionEntry,
@@ -20,6 +21,7 @@ import {
} from './entities/categorization';
import {CliCommandRenderable, DocEntryRenderable} from './entities/renderables';
import {getClassRenderable} from './transforms/class-transforms';
import {getDecoratorRenderable} from './transforms/decorator-transforms';
import {getCliRenderable} from './transforms/cli-transforms';
import {getConstantRenderable} from './transforms/constant-transforms';
import {getEnumRenderable} from './transforms/enum-transforms';
@@ -43,6 +45,9 @@ export function getRenderable(
if (isClassEntry(entry)) {
return getClassRenderable(entry, moduleName);
}
if (isDecoratorEntry(entry)) {
return getDecoratorRenderable(entry, moduleName);
}
if (isConstantEntry(entry)) {
return getConstantRenderable(entry, moduleName);
}
@@ -12,6 +12,7 @@ import {
isClassEntry,
isCliEntry,
isConstantEntry,
isDecoratorEntry,
isEnumEntry,
isFunctionEntry,
isInitializerApiFunctionEntry,
@@ -36,7 +37,7 @@ export function renderEntry(renderable: DocEntryRenderable | CliCommandRenderabl
return render(CliCommandReference(renderable));
}
if (isClassEntry(renderable) || isInterfaceEntry(renderable)) {
if (isClassEntry(renderable) || isInterfaceEntry(renderable) || isDecoratorEntry(renderable)) {
return render(ClassReference(renderable));
}
if (isConstantEntry(renderable)) {
@@ -37,18 +37,22 @@ export function codeToHtml(
});
if (options?.removeFunctionKeyword) {
return removeFunctionKeywordFromShikiHtml(html);
return replaceKeywordFromShikiHtml('function', html);
}
return html;
}
export function removeFunctionKeywordFromShikiHtml(shikiHtml: string): string {
export function replaceKeywordFromShikiHtml(
keyword: string,
shikiHtml: string,
replaceWith = '',
): string {
return (
shikiHtml
// remove the leading space of the element after the "function" element
.replace(/(<[^>]*>function<\/\w+><[^>]*>)(\s)(\w+<\/\w+>)/g, '$1$3')
.replace(new RegExp(`(<[^>]*>${keyword}<\\/\\w+><[^>]*>)(\\s)(\\w+<\\/\\w+>)`, 'g'), '$1$3')
// Shiki requires the keyword function for highlighting functions signatures
// We don't want to display it so we remove elements with the keyword
.replace(/<[^>]*>function<\/\w+>/g, '')
.replace(new RegExp(`<[^>]*>${keyword}<\\/\\w+>`, 'g'), replaceWith)
);
}
@@ -7,7 +7,7 @@
*/
import {Fragment, h} from 'preact';
import {ClassEntryRenderable} from '../entities/renderables';
import {ClassEntryRenderable, DecoratorEntryRenderable} from '../entities/renderables';
import {ClassMemberList} from './class-member-list';
import {HeaderApi} from './header-api';
import {REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes';
@@ -16,20 +16,20 @@ import {TabUsageNotes} from './tab-usage-notes';
import {TabApi} from './tab-api';
/** Component to render a class API reference document. */
export function ClassReference(entry: ClassEntryRenderable) {
export function ClassReference(entry: ClassEntryRenderable | DecoratorEntryRenderable) {
return (
<div class="api">
<HeaderApi entry={entry} />
<TabApi entry={entry} />
<TabDescription entry={entry} />
<TabUsageNotes entry={entry} />
{
entry.members.length > 0
? (<div class={REFERENCE_MEMBERS_CONTAINER}>
<ClassMemberList members={entry.members} />
</div>)
: (<></>)
}
{entry.members.length > 0 ? (
<div class={REFERENCE_MEMBERS_CONTAINER}>
<ClassMemberList members={entry.members} />
</div>
) : (
<></>
)}
</div>
);
}
@@ -19,6 +19,7 @@ import {
isClassEntry,
isClassMethodEntry,
isConstantEntry,
isDecoratorEntry,
isDeprecatedEntry,
isEnumEntry,
isFunctionEntry,
@@ -30,8 +31,8 @@ import {
} from '../entities/categorization';
import {CodeLineRenderable} from '../entities/renderables';
import {HasModuleName, HasRenderableToc} from '../entities/traits';
import {codeToHtml} from '../shiki/shiki';
import {getModuleName} from '../symbol-context';
import {codeToHtml, replaceKeywordFromShikiHtml} from '../shiki/shiki';
import {filterLifecycleMethods, mergeGettersAndSetters} from './member-transforms';
import {getLinkToModule} from './url-transforms';
@@ -65,10 +66,20 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
const metadata = mapDocEntryToCode(entry);
appendPrefixAndSuffix(entry, metadata);
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript', {
let codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript', {
removeFunctionKeyword: true,
});
if (isDecoratorEntry(entry)) {
// Shiki requires a keyword for correct formating of Decorators
// We use an interface and then replace it with a '@'
codeWithSyntaxHighlighting = replaceKeywordFromShikiHtml(
'interface',
codeWithSyntaxHighlighting,
'@',
);
}
// shiki returns the lines wrapped by 2 node : 1 pre node, 1 code node.
// As leveraging jsdom isn't trivial here, we rely on a regex to extract the line nodes
const pattern = /(.*?)<code.*?>(.*?)<\/code>(.*)/s;
@@ -126,6 +137,10 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
return getCodeTocData(members, true, isDeprecated);
}
if (isDecoratorEntry(entry)) {
return getCodeTocData(entry.members, true, isDeprecated);
}
if (isConstantEntry(entry)) {
return {
contents: `const ${entry.name}: ${entry.type};`,
@@ -454,8 +469,8 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
appendFirstAndLastLines(codeTocData, `enum ${entry.name} {`, `}`);
}
if (isInterfaceEntry(entry)) {
appendFirstAndLastLines(codeTocData, `interface ${entry.name} {`, `}`);
if (isDecoratorEntry(entry)) {
appendFirstAndLastLines(codeTocData, `interface ${entry.name} ({`, `})`);
}
}
@@ -0,0 +1,38 @@
/*!
* @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 {DecoratorEntry} from '../entities';
import {DecoratorEntryRenderable} from '../entities/renderables';
import {addRenderableCodeToc} from './code-transforms';
import {
addHtmlAdditionalLinks,
addHtmlDescription,
addHtmlJsDocTagComments,
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms';
import {addRenderableMembers} from './member-transforms';
import {addModuleName} from './module-name';
/** Given an unprocessed class entry, get the fully renderable class entry. */
export function getDecoratorRenderable(
classEntry: DecoratorEntry,
moduleName: string,
): DecoratorEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(classEntry, moduleName))),
),
),
),
),
) as DecoratorEntryRenderable;
}