@hagicode/hagilight-core 0.8.1-dev.92.1.85649f3 → 0.8.1-dev.95.1.7e01c25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/content-images.d.ts +22 -2
- package/dist/content-images.js +69 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ const links = resolveSiteLinks('en-US');
|
|
|
47
47
|
|
|
48
48
|
Images inside a `<picture>` element are skipped. An MDX `class={expression}` gets no marker because its value is not known at build time. Raw HTML `<img>` tags inside a `.md` file are not normalized; use Markdown image syntax or MDX for body images.
|
|
49
49
|
|
|
50
|
+
Local Markdown (`.md`) body images that Astro resolves are also wrapped in a `<picture>` element, with an AVIF `<source>` and a WebP `<source>` ahead of the `<img>`. Astro generates the source URLs through its image service, so AVIF needs a service that can encode it. The default sharp service does. The passthrough no-op service (`astro/assets/services/noop`) does not, so the Astro integrations skip the wrapper there and log one warning. A plugin added by hand does not check the service, and its sources then point at the original bytes. Site-root, remote, raw HTML, and MDX images are not wrapped. Encoding adds build time, which Astro caches. Each `<source>` also carries `src`, `loading`, `decoding`, `width`, and `height` copied from Astro's output. Browsers ignore these on `source`, but they are not valid HTML there.
|
|
51
|
+
|
|
50
52
|
`hagilight()` from `@hagicode/hagilight-starlight` and `hagilight()` from `@hagicode/hagilight/integration` register the plugin automatically, for the whole site and regardless of integration order. Each one opts out through its `contentImages` option; see those packages' READMEs.
|
|
51
53
|
|
|
52
54
|
To use the plugin without a feature package, add it to the Markdown processor. With the default Sätteri processor:
|
package/dist/content-images.d.ts
CHANGED
|
@@ -37,9 +37,17 @@ export interface ContentImageNode {
|
|
|
37
37
|
attributes?: MdxAttributeLike[];
|
|
38
38
|
children?: ContentImageNode[];
|
|
39
39
|
}
|
|
40
|
+
export interface LocalImageData {
|
|
41
|
+
astro?: {
|
|
42
|
+
localImagePaths?: Set<string> | readonly string[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
40
45
|
export interface SatteriContentImageContext {
|
|
46
|
+
readonly sourceFormat: string;
|
|
47
|
+
readonly data: LocalImageData;
|
|
41
48
|
parent(node: unknown): unknown;
|
|
42
49
|
setProperty(node: unknown, key: string, value: unknown): void;
|
|
50
|
+
replaceNode(node: unknown, newNode: ContentImageNode): void;
|
|
43
51
|
}
|
|
44
52
|
export interface SatteriContentImageVisitor {
|
|
45
53
|
filter: string[];
|
|
@@ -51,11 +59,19 @@ export interface SatteriContentImagePlugin {
|
|
|
51
59
|
mdxJsxFlowElement: SatteriContentImageVisitor;
|
|
52
60
|
mdxJsxTextElement: SatteriContentImageVisitor;
|
|
53
61
|
}
|
|
62
|
+
export interface PictureStep {
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
}
|
|
54
65
|
export interface ContentImageRegistrationInput {
|
|
55
66
|
config: {
|
|
56
67
|
markdown?: {
|
|
57
68
|
processor?: unknown;
|
|
58
69
|
};
|
|
70
|
+
image?: {
|
|
71
|
+
service?: {
|
|
72
|
+
entrypoint?: unknown;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
59
75
|
};
|
|
60
76
|
logger: {
|
|
61
77
|
warn(message: string): void;
|
|
@@ -72,10 +88,14 @@ export interface MdxImagePatch {
|
|
|
72
88
|
}
|
|
73
89
|
/** A class written as an expression cannot be merged statically, so its marker is left out. */
|
|
74
90
|
export declare function mdxImagePatch(attributes: readonly MdxAttributeLike[], options: ResolvedContentImageOptions): MdxImagePatch;
|
|
91
|
+
export interface UnifiedFileLike {
|
|
92
|
+
path?: unknown;
|
|
93
|
+
data?: LocalImageData;
|
|
94
|
+
}
|
|
75
95
|
/** Unified rehype plugin: `unified` processors and Astro 6 `markdown.rehypePlugins`. */
|
|
76
|
-
export declare function rehypeContentImages(options?: ContentImageOptions): (tree: ContentImageNode) => void;
|
|
96
|
+
export declare function rehypeContentImages(options?: ContentImageOptions, picture?: PictureStep): (tree: ContentImageNode, file?: UnifiedFileLike) => void;
|
|
77
97
|
/** Sätteri hast plugin: Astro's default Markdown processor, through `hastPlugins`. */
|
|
78
|
-
export declare function satteriContentImages(options?: ContentImageOptions): SatteriContentImagePlugin;
|
|
98
|
+
export declare function satteriContentImages(options?: ContentImageOptions, picture?: PictureStep): SatteriContentImagePlugin;
|
|
79
99
|
/**
|
|
80
100
|
* Register the content-image plugin on the active Markdown processor. The first enabled
|
|
81
101
|
* call appends one adapter; an `enabled: false` call opts the whole site out and removes
|
package/dist/content-images.js
CHANGED
|
@@ -109,11 +109,31 @@ function applyUnifiedTree(node, options, inPicture) {
|
|
|
109
109
|
for (const child of node.children ?? [])
|
|
110
110
|
applyUnifiedTree(child, options, childrenInPicture);
|
|
111
111
|
}
|
|
112
|
+
function wrapUnifiedPictures(node, inPicture, srcOf, nextIndex) {
|
|
113
|
+
const children = node.children;
|
|
114
|
+
if (!children)
|
|
115
|
+
return;
|
|
116
|
+
children.forEach((child, index) => {
|
|
117
|
+
if (!inPicture && child.type === 'element' && child.tagName === 'img') {
|
|
118
|
+
const src = srcOf(child);
|
|
119
|
+
if (src !== undefined) {
|
|
120
|
+
children[index] = pictureElement(child, src, nextIndex());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
wrapUnifiedPictures(child, inPicture || isPicture(child), srcOf, nextIndex);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
112
127
|
/** Unified rehype plugin: `unified` processors and Astro 6 `markdown.rehypePlugins`. */
|
|
113
|
-
export function rehypeContentImages(options = {}) {
|
|
128
|
+
export function rehypeContentImages(options = {}, picture = { enabled: true }) {
|
|
114
129
|
const resolved = resolveContentImageOptions(options);
|
|
115
|
-
|
|
130
|
+
let pictureCount = 0;
|
|
131
|
+
return (tree, file) => {
|
|
116
132
|
applyUnifiedTree(tree, resolved, false);
|
|
133
|
+
if (!picture.enabled || !String(file?.path ?? '').endsWith('.md'))
|
|
134
|
+
return;
|
|
135
|
+
const astro = file?.data?.astro;
|
|
136
|
+
wrapUnifiedPictures(tree, false, (img) => localImageSrc(img.properties?.src, astro), () => ++pictureCount);
|
|
117
137
|
};
|
|
118
138
|
}
|
|
119
139
|
function isInsidePicture(node, ctx) {
|
|
@@ -123,9 +143,37 @@ function isInsidePicture(node, ctx) {
|
|
|
123
143
|
}
|
|
124
144
|
return false;
|
|
125
145
|
}
|
|
146
|
+
const PICTURE_FORMATS = ['avif', 'webp'];
|
|
147
|
+
const NOOP_IMAGE_SERVICE = 'astro/assets/services/noop';
|
|
148
|
+
function localImageSrc(value, astro) {
|
|
149
|
+
const localPaths = astro?.localImagePaths;
|
|
150
|
+
if (typeof value !== 'string' || localPaths === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
const src = decodeURI(value);
|
|
153
|
+
const isLocal = localPaths instanceof Set ? localPaths.has(src) : localPaths.includes(src);
|
|
154
|
+
return isLocal ? src : undefined;
|
|
155
|
+
}
|
|
156
|
+
function pictureElement(img, src, index) {
|
|
157
|
+
const sources = PICTURE_FORMATS.map((format) => ({
|
|
158
|
+
type: 'element',
|
|
159
|
+
tagName: 'source',
|
|
160
|
+
properties: {
|
|
161
|
+
__ASTRO_IMAGE_: JSON.stringify({
|
|
162
|
+
src,
|
|
163
|
+
format,
|
|
164
|
+
type: `image/${format}`,
|
|
165
|
+
densities: [1],
|
|
166
|
+
index: `picture-${format}-${index}`,
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
children: [],
|
|
170
|
+
}));
|
|
171
|
+
return { type: 'element', tagName: 'picture', properties: {}, children: [...sources, img] };
|
|
172
|
+
}
|
|
126
173
|
/** Sätteri hast plugin: Astro's default Markdown processor, through `hastPlugins`. */
|
|
127
|
-
export function satteriContentImages(options = {}) {
|
|
174
|
+
export function satteriContentImages(options = {}, picture = { enabled: true }) {
|
|
128
175
|
const resolved = resolveContentImageOptions(options);
|
|
176
|
+
let pictureCount = 0;
|
|
129
177
|
const mdxVisitor = {
|
|
130
178
|
filter: ['img'],
|
|
131
179
|
visit(node, ctx) {
|
|
@@ -154,6 +202,11 @@ export function satteriContentImages(options = {}) {
|
|
|
154
202
|
ctx.setProperty(node, 'decoding', patch.decoding);
|
|
155
203
|
if (patch.classNames !== undefined)
|
|
156
204
|
ctx.setProperty(node, 'className', patch.classNames);
|
|
205
|
+
if (picture.enabled && ctx.sourceFormat === 'markdown') {
|
|
206
|
+
const src = localImageSrc(node.properties?.src, ctx.data.astro);
|
|
207
|
+
if (src !== undefined)
|
|
208
|
+
ctx.replaceNode(node, pictureElement(node, src, ++pictureCount));
|
|
209
|
+
}
|
|
157
210
|
},
|
|
158
211
|
},
|
|
159
212
|
mdxJsxFlowElement: mdxVisitor,
|
|
@@ -204,6 +257,7 @@ export function registerContentImages({ config, logger, enabled }) {
|
|
|
204
257
|
}
|
|
205
258
|
record.entry = undefined;
|
|
206
259
|
record.list = undefined;
|
|
260
|
+
record.picture = undefined;
|
|
207
261
|
return;
|
|
208
262
|
}
|
|
209
263
|
if (record.optedOut || record.entry !== undefined)
|
|
@@ -213,10 +267,14 @@ export function registerContentImages({ config, logger, enabled }) {
|
|
|
213
267
|
warnOnce(record, 'unsupported', logger, `Hagilight content images cannot run on the "${String(processor.name)}" Markdown processor. Body images are left unchanged.`);
|
|
214
268
|
return;
|
|
215
269
|
}
|
|
216
|
-
const
|
|
270
|
+
const picture = { enabled: true };
|
|
271
|
+
const plugin = processor.name === 'satteri'
|
|
272
|
+
? satteriContentImages({}, picture)
|
|
273
|
+
: (options) => rehypeContentImages(options, picture);
|
|
217
274
|
list.push(plugin);
|
|
218
275
|
record.entry = plugin;
|
|
219
276
|
record.list = list;
|
|
277
|
+
record.picture = picture;
|
|
220
278
|
}
|
|
221
279
|
/** `astro:config:done` re-check: warns once if an enabled site's adapter is missing from the final processor. */
|
|
222
280
|
export function checkContentImageRegistration({ config, logger, enabled }) {
|
|
@@ -228,7 +286,12 @@ export function checkContentImageRegistration({ config, logger, enabled }) {
|
|
|
228
286
|
const record = recordOf(processor);
|
|
229
287
|
if (record.optedOut || record.warned.has('unsupported'))
|
|
230
288
|
return;
|
|
231
|
-
if (record.entry
|
|
289
|
+
if (record.entry === undefined || !pluginListOf(processor)?.includes(record.entry)) {
|
|
290
|
+
warnOnce(record, 'lost', logger, `Hagilight content images were not found in the final "${String(processor.name)}" Markdown processor. Body images are left unchanged.`);
|
|
232
291
|
return;
|
|
233
|
-
|
|
292
|
+
}
|
|
293
|
+
if (record.picture?.enabled && config.image?.service?.entrypoint === NOOP_IMAGE_SERVICE) {
|
|
294
|
+
record.picture.enabled = false;
|
|
295
|
+
warnOnce(record, 'picture', logger, 'Hagilight content images skip the <picture> wrapper because the Astro image service is the passthrough no-op, which cannot encode AVIF. Body images keep the baseline output. Use the default sharp service to get AVIF and WebP sources.');
|
|
296
|
+
}
|
|
234
297
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-core",
|
|
3
|
-
"version": "0.8.1-dev.
|
|
3
|
+
"version": "0.8.1-dev.95.1.7e01c25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared TypeScript utilities, components, and assets for HagiCode Astro and Starlight sites",
|
|
6
6
|
"repository": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"favicon.ico"
|
|
76
76
|
],
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"astro": "
|
|
78
|
+
"astro": ">=7"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"astro": {
|