diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.ts b/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.ts index 546d8f2e762..722b88cc677 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.ts @@ -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; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.ts b/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.ts index 2df47fe4cc5..9ea1e3bbc70 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.ts @@ -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 & diff --git a/adev/shared-docs/pipeline/api-gen/rendering/processing.ts b/adev/shared-docs/pipeline/api-gen/rendering/processing.ts index 2bde3448473..1df4cd657ce 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/processing.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/processing.ts @@ -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); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts b/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts index b5d6fd67332..a054ba5ade0 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/rendering.ts @@ -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)) { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/shiki/shiki.ts b/adev/shared-docs/pipeline/api-gen/rendering/shiki/shiki.ts index 02503d9d3d6..5ab8115ed9e 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/shiki/shiki.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/shiki/shiki.ts @@ -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) ); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx index 20ad63cac33..3723c0fdae0 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx @@ -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 (
- { - entry.members.length > 0 - ? (
- -
) - : (<>) - } + {entry.members.length > 0 ? ( +
+ +
+ ) : ( + <> + )}
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts index e8dde55a863..037b4a12943 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts @@ -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( 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>(.*)/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} ({`, `})`); } } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.ts new file mode 100644 index 00000000000..cfda1fa686a --- /dev/null +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.ts @@ -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; +}