Skip to content

Commit 46269be

Browse files
SkyZeroZxkirjs
authored andcommitted
feat(docs-infra): add docs-image extension for enhanced image handling in markdown
Extends the Markdown parser to support `loading`, `decoding`, and `fetchpriority` attributes for images via curly brace syntax.
1 parent 6f1329f commit 46269be

7 files changed

Lines changed: 109 additions & 4 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {TokenizerThis, Tokens} from 'marked';
10+
11+
export interface DocsImage extends Tokens.Image {
12+
loading?: string;
13+
decoding?: string;
14+
fetchpriority?: string;
15+
}
16+
17+
// Regex to match image syntax: ![alt](url {attrs} 'title') or ![alt](url 'title')
18+
const imageRule = /^!\[([^\]]*)\]\(([^)\s]+)(?:\s+\{([^}]+)\})?(?:\s+['"]([^'"]+)['"])?\)/;
19+
20+
export const docsImageExtension = {
21+
name: 'docs-image',
22+
level: 'inline' as const,
23+
start(src: string) {
24+
return src.match(/!\[/)?.index;
25+
},
26+
tokenizer(this: TokenizerThis, src: string): DocsImage | undefined {
27+
const match = imageRule.exec(src);
28+
if (match) {
29+
const text = match[1];
30+
const href = match[2];
31+
const metadataStr = match[3] || '';
32+
const title = match[4] || null;
33+
34+
const loadingRule = /loading\s*:\s*(['"`])([^'"`]+)\1/;
35+
const decodingRule = /decoding\s*:\s*(['"`])([^'"`]+)\1/;
36+
const fetchpriorityRule = /fetchpriority\s*:\s*(['"`])([^'"`]+)\1/;
37+
38+
const token: DocsImage = {
39+
type: 'image',
40+
raw: match[0],
41+
href,
42+
title,
43+
text,
44+
tokens: [],
45+
loading: loadingRule.exec(metadataStr)?.[2],
46+
decoding: decodingRule.exec(metadataStr)?.[2],
47+
fetchpriority: fetchpriorityRule.exec(metadataStr)?.[2],
48+
};
49+
return token;
50+
}
51+
return undefined;
52+
},
53+
};

adev/shared-docs/pipeline/shared/marked/parse.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import {docsCodeBlockExtension} from './extensions/docs-code/docs-code-block.mjs
2323
import {docsCodeExtension, DocsCodeToken} from './extensions/docs-code/docs-code.mjs';
2424
import {docsCodeMultifileExtension} from './extensions/docs-code/docs-code-multifile.mjs';
2525
import {docsTabGroupExtension, docsTabExtension} from './extensions/docs-tabs.mjs';
26+
import {docsImageExtension} from './extensions/docs-image.mjs';
27+
2628
import {hooks} from './hooks.mjs';
2729

2830
let markedInstance: typeof marked;
2931
const extensions = [
32+
docsImageExtension,
3033
docsAlertExtension,
3134
docsCalloutExtension,
3235
docsPillExtension,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
![New Logo!](https://angular.dev/favicon.ico 'Our new icon')
22
![New Logo!](./some-image.png 'Local Image')
3+
![Lazy Image](https://angular.dev/assets/logo.svg {loading: 'lazy'})
4+
![Async Decoded Image](https://angular.dev/assets/logo.svg {decoding: 'async'} 'Async Image')
5+
![High Priority Image](https://angular.dev/assets/logo.svg {fetchpriority: 'high'})
6+
![Combined Attributes](https://angular.dev/assets/logo.svg {loading: 'eager', decoding: 'sync', fetchpriority: 'high'} 'Hero Image')

adev/shared-docs/pipeline/shared/marked/test/image/image.spec.mts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,28 @@ describe('markdown to html', () => {
2929
const image = markdownDocument.querySelector('img[title="Local Image"]');
3030
expect(image?.getAttribute('src')).toBe('unknown/some-image.png');
3131
});
32+
33+
it('should add loading attribute when specified', () => {
34+
const image = markdownDocument.querySelectorAll('img')[2];
35+
expect(image?.getAttribute('loading')).toBe('lazy');
36+
});
37+
38+
it('should add decoding attribute when specified', () => {
39+
const image = markdownDocument.querySelectorAll('img')[3];
40+
expect(image?.getAttribute('decoding')).toBe('async');
41+
expect(image?.getAttribute('title')).toBe('Async Image');
42+
});
43+
44+
it('should add fetchpriority attribute when specified', () => {
45+
const image = markdownDocument.querySelectorAll('img')[4];
46+
expect(image?.getAttribute('fetchpriority')).toBe('high');
47+
});
48+
49+
it('should handle multiple attributes with title', () => {
50+
const image = markdownDocument.querySelectorAll('img')[5];
51+
expect(image?.getAttribute('loading')).toBe('eager');
52+
expect(image?.getAttribute('decoding')).toBe('sync');
53+
expect(image?.getAttribute('fetchpriority')).toBe('high');
54+
expect(image?.getAttribute('title')).toBe('Hero Image');
55+
});
3256
});

adev/shared-docs/pipeline/shared/marked/transformations/image.mts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,31 @@
88

99
import {normalize} from 'path';
1010

11-
import {Renderer, Tokens} from 'marked';
11+
import {Renderer} from 'marked';
12+
import type {DocsImage} from '../extensions/docs-image.mjs';
1213

1314
// TODO(josephperrott): Determine how we can define/know the image content base path.
1415
const imageContentBasePath = 'unknown';
1516

16-
export function imageRender(this: Renderer, {href, title, text}: Tokens.Image) {
17+
export function imageRender(this: Renderer, token: DocsImage) {
18+
const {href, title, text, loading, decoding, fetchpriority} = token;
19+
1720
const isRelativeSrc = href?.startsWith('./');
1821
const src = isRelativeSrc ? `${imageContentBasePath}/${normalize(href)}` : href;
22+
23+
const attrs = [
24+
`src="${src}"`,
25+
`alt="${text}"`,
26+
`class="docs-image"`,
27+
title ? `title="${title}"` : null,
28+
loading ? `loading="${loading}"` : null,
29+
decoding ? `decoding="${decoding}"` : null,
30+
fetchpriority ? `fetchpriority="${fetchpriority}"` : null,
31+
]
32+
.filter(Boolean)
33+
.join(' ');
34+
1935
return `
20-
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
36+
<img ${attrs}>
2137
`;
2238
}

adev/src/content/events/v21.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
![A retro 8-bit, pixel art style graphic announcing the upcoming release of Angular v21. The large, gradient 'v21' text dominates the frame. Next to it, in smaller text, are the words 'The Adventure Begins' and the release date '11-20-2025' inside a pink pixelated box. The Angular logo is in the bottom right corner.](assets/images/v21-event/angular-v21-hero.jpg 'Angular v21 Hero Image')
1+
![A retro 8-bit, pixel art style graphic announcing the upcoming release of Angular v21. The large, gradient 'v21' text dominates the frame. Next to it, in smaller text, are the words 'The Adventure Begins' and the release date '11-20-2025' inside a pink pixelated box. The Angular logo is in the bottom right corner.](assets/images/angular-v21-hero.jpg {loading: 'eager', fetchpriority: 'high'} 'Angular v21 Hero Image')
22

33
# Angular v21: The Adventure Begins
44

adev/src/content/kitchen-sink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ You can add images using the semantic Markdown image:
321321
![Rhubarb the small cat](assets/images/kitchen-sink/rhubarb.jpg#small)
322322
![Rhubarb the medium cat](assets/images/kitchen-sink/rhubarb.jpg#medium)
323323

324+
## Add attributes using curly braces syntax
325+
326+
![Lazy loaded image](assets/images/kitchen-sink/rhubarb.jpg {loading: 'lazy'})
327+
![Combined attributes](assets/images/kitchen-sink/rhubarb.jpg#small {loading: 'lazy', decoding: 'async', fetchpriority: 'low'})
328+
324329
Embedded videos are created with `docs-video` and just need a `src` and `alt`:
325330

326331
<docs-video src="https://www.youtube.com/embed/O47uUnJjbJc" alt=""/>

0 commit comments

Comments
 (0)