docs(docs-infra): prefer/avoid code block styling

fixes #65866

(cherry picked from commit 3411638f65)
This commit is contained in:
Matthieu Riegler 2025-12-05 13:46:51 +01:00 committed by Alex Rickabaugh
parent 5880fbc73c
commit 182116582e
9 changed files with 111 additions and 22 deletions

View file

@ -205,6 +205,7 @@ export class DocViewer {
const preview = Boolean(placeholder.getAttribute('preview'));
const hideCode = Boolean(placeholder.getAttribute('hideCode'));
const title = placeholder.getAttribute('header') ?? undefined;
const style = placeholder.getAttribute('style') ?? undefined;
const firstCodeSnippetTitle =
snippets.length > 0 ? (snippets[0].title ?? snippets[0].name) : undefined;
const exampleRef = this.viewContainer.createComponent(ExampleViewer);
@ -217,6 +218,7 @@ export class DocViewer {
preview,
hideCode,
id: this.countOfExamples,
style,
});
exampleRef.setInput('githubUrl', `${GITHUB_CONTENT_URL}/${snippets[0].name}`);

View file

@ -347,6 +347,7 @@ const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
preview: false,
hideCode: false,
...value,
style: undefined,
};
};

View file

@ -42,4 +42,6 @@ export interface ExampleMetadata {
preview: boolean;
/** Whether to hide code example by default. */
hideCode: boolean;
/** Visual style for the code example */
style?: 'prefer' | 'avoid';
}

View file

@ -16,6 +16,7 @@ export interface DocsCodeBlock extends CodeToken {
code: string;
// Code language
language: string | undefined;
style: 'prefer' | 'avoid' | undefined;
}
/**
@ -40,6 +41,7 @@ export const docsCodeBlockExtension = {
const headerRule = /header\s*:\s*(['"`])([^'"`]+)\1/; // The 2nd capture matters here
const highlightRule = /highlight\s*:\s*(.*)([^,])/;
const hideCopyRule = /hideCopy/;
const preferRule = /(prefer|avoid)/;
const token: DocsCodeBlock = {
raw: match[0],
@ -49,6 +51,7 @@ export const docsCodeBlockExtension = {
header: headerRule.exec(metadataStr)?.[2],
highlight: highlightRule.exec(metadataStr)?.[1],
hideCopy: hideCopyRule.test(metadataStr),
style: preferRule.exec(metadataStr)?.[1] as 'prefer' | 'avoid' | undefined,
};
return token;
}

View file

@ -32,6 +32,7 @@ const visibleLinesRule = /visibleLines="([^"]*)"/;
const regionRule = /region="([^"]*)"/;
const previewRule = /preview/;
const hideCodeRule = /hideCode/;
const preferRule = /(prefer|avoid)/;
export const docsCodeExtension = {
name: 'docs-code',
@ -54,6 +55,7 @@ export const docsCodeExtension = {
const preview = previewRule.exec(attr) ? true : false;
const hideCode = hideCodeRule.exec(attr) ? true : false;
const classes = classRule.exec(attr);
const style = preferRule.exec(attr);
let code = match[2]?.trim() ?? '';
if (path && path[1]) {
@ -76,6 +78,7 @@ export const docsCodeExtension = {
region: region?.[1],
preview: preview,
hideCode,
style: style?.[1] as 'prefer' | 'avoid' | undefined,
classes: classes?.[1]?.split(' '),
};
return token;

View file

@ -53,7 +53,7 @@ export function formatCode(token: CodeToken, context: RendererContext): string {
highlightCode(context.highlighter, token);
const containerEl = JSDOM.fragment(`
<div class="docs-code">
<div class="docs-code${token.style ? ' docs-code-' + token.style : ''}">
${buildHeaderElement(token)}
${token.code}
</div>
@ -95,7 +95,18 @@ export function processForApiLinks(fragment: Element, apiEntries: ApiEntries): v
/** Build the header element if a header is provided in the token. */
function buildHeaderElement(token: CodeToken) {
return token.header ? `<div class="docs-code-header"><h3>${token.header}</h3></div>` : '';
let header = '';
if (token.style) {
header += `<span class="docs-code-header-style ">${token.style === 'prefer' ? 'Prefer' : 'Avoid'}</span>`;
}
if (token.header) {
header += `<h3>${token.header}</h3>`;
}
if (!header) return '';
return `<div class="docs-code-header">${header}</div>`;
}
function applyContainerAttributesAndClasses(el: Element, token: CodeToken) {

View file

@ -166,6 +166,18 @@ $code-font-size: 0.875rem;
border-width: 0;
border-radius: 0;
}
&.docs-code-avoid {
--style-color: var(--hot-red);
}
&.docs-code-prefer {
--style-color: var(--symbolic-green);
}
&.docs-code-avoid,
&.docs-code-prefer {
border-color: color-mix(in srgb, var(--style-color) 40%, var(--senary-contrast));
}
}
.shiki {
@ -278,18 +290,51 @@ $code-font-size: 0.875rem;
}
.docs-code-header {
display: flex;
padding: 0.75rem;
// docs header background
&::before {
content: '';
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: var(--subtle-purple);
border-radius: 0.25rem 0.25rem 0 0;
transition: background 0.3s ease;
background: var(--subtle-purple);
border-radius: 0.25rem 0.25rem 0 0;
transition: background 0.3s ease;
.docs-code-avoid &,
.docs-code-prefer & {
background: color-mix(in srgb, var(--style-color) 17%, var(--page-background));
color: color-mix(in srgb, var(--style-color) 60%, var(--primary-contrast));
h3 {
color: color-mix(in srgb, var(--style-color) 60%, var(--primary-contrast));
}
}
.docs-code-header-style {
display: flex;
align-items: center;
font-size: 0.8rem;
&::before {
font-family: var(--icons);
margin-right: 0.5rem;
}
&:not(:last-child)::after {
content: '-';
margin: 0 0.5rem;
}
}
.docs-code-header-style {
.docs-code-prefer & {
&::before {
content: 'check';
}
}
}
.docs-code-header-style {
.docs-code-avoid & {
&::before {
content: 'dangerous';
}
}
}
}

View file

@ -219,16 +219,15 @@ export class UserProfile {
Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgClass) and [`NgStyle`](/api/common/NgStyle) directives.
```html
<!-- PREFER -->
```html {prefer}
<div [class.admin]="isAdmin" [class.dense]="density === 'high'">
<div [style.color]="textColor" [style.background-color]="backgroundColor">
<!-- OR -->
<div [class]="{admin: isAdmin, dense: density === 'high'}">
<div [style]="{'color': textColor, 'background-color': backgroundColor}">
```
<!-- AVOID -->
```html {avoid}
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">
<div [ngStyle]="{'color': textColor, 'background-color': backgroundColor}">
```
@ -246,11 +245,11 @@ For more details, refer to the [bindings guide](/guide/templates/binding#css-cla
Prefer naming event handlers for the action they perform rather than for the triggering event:
```html
<!-- PREFER -->
```html {prefer}
<button (click)="saveUserData()">Save</button>
```
<!-- AVOID -->
```html {avoid}
<button (click)="handleClick()">Save</button>
```
@ -291,14 +290,14 @@ well-named methods to contain that logic and then _call those methods_ in your l
Lifecycle hook names describe _when_ they run, meaning that the code inside doesn't have a
meaningful name that describes what the code inside is doing.
```typescript
// PREFER
```ts {prefer}
ngOnInit() {
this.startLogging();
this.runBackgroundTask();
}
```
// AVOID
```ts {avoid}
ngOnInit() {
this.logger.setMode('info');
this.logger.monitorErrors();

View file

@ -364,3 +364,26 @@ This can be used to separate page sections, like we're about to do below. These
<hr/>
The end!
## Prefer / Avoid
```ts {prefer}
const foo= 'bar';
```
```ts {avoid}
const bar = 'foo';
```
```ts {avoid, header: 'with a header'}
const baz = 42;
```
<docs-code
path="adev/src/content/examples/hello-world/src/app/app.component-old.ts"
header="A styled code example"
language='ts'
linenums
highlight="[[3,7], 9]"
prefer>
</docs-code>