From 31c689a5f4d8bf1e85483fd987dbc0181098507a Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Mon, 1 Sep 2025 23:21:21 +0200 Subject: [PATCH] docs(docs-infra): highlight & parse api descriptions. (#63536) The prevent future regression of this, this commit introduces additional tests. PR Close #63536 --- .../pipeline/api-gen/rendering/BUILD.bazel | 4 ++ .../api-gen/rendering/symbol-context.mts | 4 ++ .../api-gen/rendering/test/marked.spec.mts | 6 ++- .../test/transforms/jsdoc-transforms.spec.mts | 40 ++++++++++++++++++- .../rendering/transforms/cli-transforms.mts | 6 ++- .../rendering/transforms/jsdoc-transforms.mts | 16 ++++++-- .../extensions/docs-code/format/index.mts | 2 +- .../pipeline/shared/marked/renderer.mts | 7 ++-- .../shared/marked/transformations/code.mts | 15 ++++++- .../shared/marked/transformations/heading.mts | 12 ++++-- 10 files changed, 96 insertions(+), 16 deletions(-) diff --git a/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel b/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel index 36457fa2e53..d27e71d1fec 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel +++ b/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel @@ -5,6 +5,10 @@ package(default_visibility = ["//adev/shared-docs/pipeline/api-gen:__subpackages esbuild( name = "bin", entry_point = ":index.mts", + external = [ + "jsdom", + "playwright-core", + ], format = "esm", output = "bin.mjs", platform = "node", diff --git a/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.mts b/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.mts index e50bf3012aa..3477eef3067 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/symbol-context.mts @@ -21,6 +21,10 @@ export function setCurrentSymbol(symbol: string): void { currentSymbol = symbol; } +export function getSymbols() { + return symbols; +} + export function getCurrentSymbol(): string | undefined { return currentSymbol; } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts index bb7cb8bb140..2c2a0cf9c2a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/marked.spec.mts @@ -79,12 +79,14 @@ describe('markdown to html', () => { // In the description const descriptionItem = entry.querySelector('.docs-reference-description')!; - expect(descriptionItem.innerHTML).toContain('afterRender'); + expect(descriptionItem.innerHTML).toContain( + 'afterRender', + ); // In the card const cardItem = entry.querySelectorAll('.docs-reference-card-item')[1]; expect(cardItem.innerHTML).toContain( - 'AfterRenderPhase.MixedReadWrite', + 'AfterRenderPhase.MixedReadWrite', ); }); }); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts index c38e6b96894..6fd406d558c 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import {initHighlighter} from '../../../../shared/shiki.mjs'; +import {setHighlighterInstance} from '../../shiki/shiki.mjs'; import {setCurrentSymbol, setSymbols} from '../../symbol-context.mjs'; -import {addHtmlAdditionalLinks} from '../../transforms/jsdoc-transforms.mjs'; +import {addHtmlAdditionalLinks, addHtmlDescription} from '../../transforms/jsdoc-transforms.mjs'; // @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context. describe('jsdoc transforms', () => { @@ -151,4 +153,40 @@ describe('jsdoc transforms', () => { expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/); }); + + it('should parse markdown in descriptions', async () => { + setHighlighterInstance(await initHighlighter()); + + setSymbols( + Object.fromEntries([ + ['Route', 'test'], + ['Router', 'angular/router'], + ['Router.someMethod', 'test'], + ['Router.someMethodWithParenthesis', 'test'], + ['FormGroup', 'test'], + ['FormGroup.someMethod', 'test'], + ]), + ); + + const entry = addHtmlDescription({ + description: ` +\`\`\`angular-ts +import { Router } from '@angular/router'; + +function setupRouter() { + const router = inject(Router); +} +\`\`\` + `, + moduleName: 'test', + }); + + // Should have some shiki variables (meaning the description was highlighted). + expect(entry.htmlDescription).toContain('--shiki'); + + // Having docs-code means that the description was parsed and formatted correctly (by the shared marked renderer) + expect(entry.htmlDescription).toContain('class="docs-code"'); + + expect(entry.htmlDescription).toContain('/api/angular/router/Router'); + }); }); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/cli-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/cli-transforms.mts index be99e34f4d5..97049aa117e 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/cli-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/cli-transforms.mts @@ -14,13 +14,17 @@ import { CliCommandRenderable, CliOptionRenderable, } from '../entities/renderables.mjs'; +import {parseMarkdown} from '../../../shared/marked/parse.mjs'; +import {getHighlighterInstance} from '../shiki/shiki.mjs'; /** Given an unprocessed CLI entry, get the fully renderable CLI entry. */ export function getCliRenderable(command: CliCommand): CliCommandRenderable { return { ...command, subcommands: command.subcommands?.map((sub) => getCliRenderable(sub)), - htmlDescription: marked.parse(command.longDescription ?? command.shortDescription) as string, + htmlDescription: parseMarkdown(command.longDescription ?? command.shortDescription, { + highlighter: getHighlighterInstance(), + }), cards: getCliCardsRenderable(command), argumentsLabel: getArgumentsLabel(command), hasOptions: getOptions(command).length > 0, diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts index 576f0479cda..4da5fdca1f4 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {marked} from 'marked'; import {JsDocTagEntry} from '../entities.mjs'; import {getDeprecatedEntry, getTagSinceVersion} from '../entities/categorization.mjs'; @@ -26,7 +25,14 @@ import { } from '../entities/traits.mjs'; import {addApiLinksToHtml} from './code-transforms.mjs'; -import {getCurrentSymbol, getSymbolUrl, unknownSymbolMessage} from '../symbol-context.mjs'; +import { + getCurrentSymbol, + getSymbols, + getSymbolUrl, + unknownSymbolMessage, +} from '../symbol-context.mjs'; +import {parseMarkdown} from '../../../shared/marked/parse.mjs'; +import {getHighlighterInstance} from '../shiki/shiki.mjs'; const JS_DOC_USAGE_NOTE_TAGS: Set = new Set(['remarks', 'usageNotes', 'example']); export const JS_DOC_SEE_TAG = 'see'; @@ -99,7 +105,11 @@ export function addHtmlUsageNotes(entry: T): T & HasHtml /** Given a markdown JsDoc text, gets the rendered HTML. */ function getHtmlForJsDocText(text: string): string { - const parsed = marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text))) as string; + const mdToParse = convertLinks(wrapExampleHtmlElementsWithCode(text)); + const parsed = parseMarkdown(mdToParse, { + apiEntries: getSymbols(), + highlighter: getHighlighterInstance(), + }); return addApiLinksToHtml(parsed); } diff --git a/adev/shared-docs/pipeline/shared/marked/extensions/docs-code/format/index.mts b/adev/shared-docs/pipeline/shared/marked/extensions/docs-code/format/index.mts index da4f6791239..1eb2e6d1c77 100644 --- a/adev/shared-docs/pipeline/shared/marked/extensions/docs-code/format/index.mts +++ b/adev/shared-docs/pipeline/shared/marked/extensions/docs-code/format/index.mts @@ -63,7 +63,7 @@ export function formatCode(token: CodeToken, context: RendererContext): string { `).firstElementChild!; applyContainerAttributesAndClasses(containerEl, token); - processForApiLinks(containerEl, context.apiEntries); + processForApiLinks(containerEl, context.apiEntries ?? {}); return containerEl.outerHTML; } diff --git a/adev/shared-docs/pipeline/shared/marked/renderer.mts b/adev/shared-docs/pipeline/shared/marked/renderer.mts index dd19aaccffc..131fd154f07 100644 --- a/adev/shared-docs/pipeline/shared/marked/renderer.mts +++ b/adev/shared-docs/pipeline/shared/marked/renderer.mts @@ -13,12 +13,12 @@ import {listRender} from './transformations/list.mjs'; import {imageRender} from './transformations/image.mjs'; import {textRender} from './transformations/text.mjs'; import {headingRender} from './transformations/heading.mjs'; -import {codespanRender} from './transformations/code.mjs'; +import {codeRender, codespanRender} from './transformations/code.mjs'; import {HighlighterGeneric} from 'shiki'; export interface RendererContext { - markdownFilePath: string; - apiEntries: Record; + markdownFilePath?: string; + apiEntries?: Record; highlighter: HighlighterGeneric; } @@ -40,4 +40,5 @@ export class AdevDocsRenderer extends Renderer { override text = textRender; override heading = headingRender; override codespan = codespanRender; + override code = codeRender; } diff --git a/adev/shared-docs/pipeline/shared/marked/transformations/code.mts b/adev/shared-docs/pipeline/shared/marked/transformations/code.mts index e7e159a22c4..48df124d4b5 100644 --- a/adev/shared-docs/pipeline/shared/marked/transformations/code.mts +++ b/adev/shared-docs/pipeline/shared/marked/transformations/code.mts @@ -9,9 +9,10 @@ import {Tokens} from 'marked'; import {AdevDocsRenderer} from '../renderer.mjs'; import {getSymbolUrl} from '../../linking.mjs'; +import {codeToHtml} from '../../shiki.mjs'; export function codespanRender(this: AdevDocsRenderer, token: Tokens.Codespan) { - const apiLink = getSymbolUrl(token.text, this.context.apiEntries); + const apiLink = getSymbolUrl(token.text, this.context.apiEntries ?? {}); if (apiLink) { const htmlToken: Tokens.HTML = { type: 'html', @@ -31,3 +32,15 @@ export function codespanRender(this: AdevDocsRenderer, token: Tokens.Codespan) { } return this.defaultRenderer.codespan(token); } + +export function codeRender(this: AdevDocsRenderer, {text, lang}: Tokens.Code): string { + const highlightResult = codeToHtml(this.context.highlighter, text, lang) + // remove spaces/line-breaks between elements to not mess-up `pre` style + .replace(/>\s+<'); + + return ` +
+ ${highlightResult} +
+ `; +} diff --git a/adev/shared-docs/pipeline/shared/marked/transformations/heading.mts b/adev/shared-docs/pipeline/shared/marked/transformations/heading.mts index 5eeb33aa4ac..b58f207e99a 100644 --- a/adev/shared-docs/pipeline/shared/marked/transformations/heading.mts +++ b/adev/shared-docs/pipeline/shared/marked/transformations/heading.mts @@ -17,7 +17,7 @@ export function headingRender(this: AdevDocsRenderer, {depth, tokens}: Tokens.He export function formatHeading( {text, depth}: {text: string; depth: number}, - markdownFilePath: string, + markdownFilePath?: string, ): string { if (depth === 1) { return ` @@ -55,14 +55,18 @@ export function formatHeading( const GITHUB_EDIT_CONTENT_URL = 'https://github.com/angular/angular/edit/main'; /** Get the page title with edit button to modify the page source. */ -export function getPageTitle(text: string, filePath: string): string { +export function getPageTitle(text: string, filePath?: string): string { return `

${text}

- + ${ + filePath + ? ` edit - + ` + : '' + }
`; }