@hagicode/hagilight-core 0.9.1-dev.100.1.8af2d7a → 0.9.1-dev.98.1.e071fc7
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 +1 -1
- package/dist/content-images.d.ts +0 -20
- package/dist/content-images.js +12 -205
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ 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
|
|
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
51
|
|
|
52
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.
|
|
53
53
|
|
package/dist/content-images.d.ts
CHANGED
|
@@ -36,35 +36,18 @@ export interface ContentImageNode {
|
|
|
36
36
|
properties?: Record<string, unknown>;
|
|
37
37
|
attributes?: MdxAttributeLike[];
|
|
38
38
|
children?: ContentImageNode[];
|
|
39
|
-
/** Source text of an `mdxjsEsm` node. */
|
|
40
|
-
value?: string;
|
|
41
|
-
/** ESTree data of an `mdxjsEsm` node, read by the unified MDX compiler. */
|
|
42
|
-
data?: unknown;
|
|
43
|
-
}
|
|
44
|
-
/** Per-document state for MDX picture output on the Sätteri processor. */
|
|
45
|
-
export interface MdxPictureScope {
|
|
46
|
-
/** Decoded `src` to the identifier its asset import binds. */
|
|
47
|
-
assets: Map<string, string>;
|
|
48
|
-
/** Whether the `Picture` component import has been queued for this document. */
|
|
49
|
-
component: boolean;
|
|
50
|
-
/** Import nodes not yet placed in the document. */
|
|
51
|
-
pending: ContentImageNode[];
|
|
52
39
|
}
|
|
53
40
|
export interface LocalImageData {
|
|
54
41
|
astro?: {
|
|
55
42
|
localImagePaths?: Set<string> | readonly string[];
|
|
56
43
|
};
|
|
57
|
-
/** Sätteri's per-document MDX picture state, created on first use. */
|
|
58
|
-
hagilightContentPicture?: MdxPictureScope;
|
|
59
44
|
}
|
|
60
45
|
export interface SatteriContentImageContext {
|
|
61
46
|
readonly sourceFormat: string;
|
|
62
|
-
readonly fileURL?: URL | undefined;
|
|
63
47
|
readonly data: LocalImageData;
|
|
64
48
|
parent(node: unknown): unknown;
|
|
65
49
|
setProperty(node: unknown, key: string, value: unknown): void;
|
|
66
50
|
replaceNode(node: unknown, newNode: ContentImageNode): void;
|
|
67
|
-
prependChild(node: unknown, childNode: ContentImageNode | ContentImageNode[]): void;
|
|
68
51
|
}
|
|
69
52
|
export interface SatteriContentImageVisitor {
|
|
70
53
|
filter: string[];
|
|
@@ -78,8 +61,6 @@ export interface SatteriContentImagePlugin {
|
|
|
78
61
|
}
|
|
79
62
|
export interface PictureStep {
|
|
80
63
|
enabled: boolean;
|
|
81
|
-
/** Filesystem path of the project's `publicDir`. Images that resolve into it keep their markup. */
|
|
82
|
-
publicDir?: string;
|
|
83
64
|
}
|
|
84
65
|
export interface ContentImageRegistrationInput {
|
|
85
66
|
config: {
|
|
@@ -91,7 +72,6 @@ export interface ContentImageRegistrationInput {
|
|
|
91
72
|
entrypoint?: unknown;
|
|
92
73
|
};
|
|
93
74
|
};
|
|
94
|
-
publicDir?: unknown;
|
|
95
75
|
};
|
|
96
76
|
logger: {
|
|
97
77
|
warn(message: string): void;
|
package/dist/content-images.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
1
|
export const CONTENT_IMAGE_CLASS = 'hagilight-content-image';
|
|
4
2
|
function isRecord(value) {
|
|
5
3
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -72,7 +70,7 @@ function isMdxJsx(node) {
|
|
|
72
70
|
}
|
|
73
71
|
function isPicture(node) {
|
|
74
72
|
return (node.type === 'element' && node.tagName === 'picture')
|
|
75
|
-
|| (isMdxJsx(node) &&
|
|
73
|
+
|| (isMdxJsx(node) && node.name === 'picture');
|
|
76
74
|
}
|
|
77
75
|
function upsertMdxAttribute(attributes, name, value) {
|
|
78
76
|
const existing = attributes.find((attribute) => attribute.name === name);
|
|
@@ -111,20 +109,19 @@ function applyUnifiedTree(node, options, inPicture) {
|
|
|
111
109
|
for (const child of node.children ?? [])
|
|
112
110
|
applyUnifiedTree(child, options, childrenInPicture);
|
|
113
111
|
}
|
|
114
|
-
|
|
115
|
-
function replaceUnifiedImages(node, inPicture, replace) {
|
|
112
|
+
function wrapUnifiedPictures(node, inPicture, srcOf, nextIndex) {
|
|
116
113
|
const children = node.children;
|
|
117
114
|
if (!children)
|
|
118
115
|
return;
|
|
119
116
|
children.forEach((child, index) => {
|
|
120
117
|
if (!inPicture && child.type === 'element' && child.tagName === 'img') {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
children[index] =
|
|
118
|
+
const src = srcOf(child);
|
|
119
|
+
if (src !== undefined) {
|
|
120
|
+
children[index] = pictureElement(child, src, nextIndex());
|
|
124
121
|
return;
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
|
-
|
|
124
|
+
wrapUnifiedPictures(child, inPicture || isPicture(child), srcOf, nextIndex);
|
|
128
125
|
});
|
|
129
126
|
}
|
|
130
127
|
/** Unified rehype plugin for sites whose `markdown.processor` is `unified()`. */
|
|
@@ -133,28 +130,10 @@ export function rehypeContentImages(options = {}, picture = { enabled: true }) {
|
|
|
133
130
|
let pictureCount = 0;
|
|
134
131
|
return (tree, file) => {
|
|
135
132
|
applyUnifiedTree(tree, resolved, false);
|
|
136
|
-
|
|
137
|
-
if (!picture.enabled || format === undefined)
|
|
133
|
+
if (!picture.enabled || !String(file?.path ?? '').endsWith('.md'))
|
|
138
134
|
return;
|
|
139
135
|
const astro = file?.data?.astro;
|
|
140
|
-
|
|
141
|
-
const eligibleSrc = (img) => pictureSource(img.properties?.src, astro, picture, documentPath);
|
|
142
|
-
if (format === 'markdown') {
|
|
143
|
-
replaceUnifiedImages(tree, false, (img) => {
|
|
144
|
-
const src = eligibleSrc(img);
|
|
145
|
-
return src === undefined ? undefined : pictureElement(img, src, ++pictureCount);
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const scope = newMdxPictureScope();
|
|
150
|
-
replaceUnifiedImages(tree, false, (img) => {
|
|
151
|
-
const src = eligibleSrc(img);
|
|
152
|
-
if (src === undefined)
|
|
153
|
-
return undefined;
|
|
154
|
-
return pictureComponent(pictureProperties(img.properties ?? {}, resolved), assetIdentifier(scope, src, 'estree'), 'estree');
|
|
155
|
-
});
|
|
156
|
-
if (scope.pending.length > 0)
|
|
157
|
-
tree.children?.unshift(...scope.pending);
|
|
136
|
+
wrapUnifiedPictures(tree, false, (img) => localImageSrc(img.properties?.src, astro), () => ++pictureCount);
|
|
158
137
|
};
|
|
159
138
|
}
|
|
160
139
|
function isInsidePicture(node, ctx) {
|
|
@@ -165,12 +144,7 @@ function isInsidePicture(node, ctx) {
|
|
|
165
144
|
return false;
|
|
166
145
|
}
|
|
167
146
|
const PICTURE_FORMATS = ['avif', 'webp'];
|
|
168
|
-
const PICTURE_FALLBACK_FORMAT = 'webp';
|
|
169
|
-
const PICTURE_COMPONENT = 'HagilightContentPicture';
|
|
170
|
-
const PICTURE_COMPONENT_SOURCE = 'astro:assets';
|
|
171
147
|
const NOOP_IMAGE_SERVICE = 'astro/assets/services/noop';
|
|
172
|
-
/** Extensions the picture step leaves alone: SVG is already vector, and a GIF would lose its animation. */
|
|
173
|
-
const VECTOR_OR_ANIMATED = /\.(?:svg|gif)$/iu;
|
|
174
148
|
function localImageSrc(value, astro) {
|
|
175
149
|
const localPaths = astro?.localImagePaths;
|
|
176
150
|
if (typeof value !== 'string' || localPaths === undefined)
|
|
@@ -179,59 +153,6 @@ function localImageSrc(value, astro) {
|
|
|
179
153
|
const isLocal = localPaths instanceof Set ? localPaths.has(src) : localPaths.includes(src);
|
|
180
154
|
return isLocal ? src : undefined;
|
|
181
155
|
}
|
|
182
|
-
/** A file URL or absolute path as a filesystem path; `undefined` for anything else. */
|
|
183
|
-
function toFilePath(value) {
|
|
184
|
-
const text = value instanceof URL ? value.href : value;
|
|
185
|
-
if (typeof text !== 'string')
|
|
186
|
-
return undefined;
|
|
187
|
-
if (text.startsWith('file:'))
|
|
188
|
-
return fileURLToPath(text);
|
|
189
|
-
return isAbsolute(text) ? text : undefined;
|
|
190
|
-
}
|
|
191
|
-
function documentFormat(filePath) {
|
|
192
|
-
const text = String(filePath ?? '');
|
|
193
|
-
if (text.endsWith('.mdx'))
|
|
194
|
-
return 'mdx';
|
|
195
|
-
if (text.endsWith('.md'))
|
|
196
|
-
return 'markdown';
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
function withoutQueryOrFragment(src) {
|
|
200
|
-
return src.replace(/[?#].*$/u, '');
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* The file a relative reference names, resolved against the document's directory.
|
|
204
|
-
* Site-root, bare and aliased specifiers, and a missing document location, resolve to `undefined`.
|
|
205
|
-
*/
|
|
206
|
-
function resolveLocalFile(src, documentPath) {
|
|
207
|
-
if (documentPath === undefined)
|
|
208
|
-
return undefined;
|
|
209
|
-
const file = withoutQueryOrFragment(src);
|
|
210
|
-
if (!file.startsWith('./') && !file.startsWith('../'))
|
|
211
|
-
return undefined;
|
|
212
|
-
return resolve(dirname(documentPath), file);
|
|
213
|
-
}
|
|
214
|
-
function isInsideDirectory(directory, file) {
|
|
215
|
-
const offset = relative(directory, file);
|
|
216
|
-
return offset !== '..' && !offset.startsWith(`..${sep}`) && !isAbsolute(offset);
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* The decoded `src` when the picture step may wrap this image, otherwise `undefined`. One rule for
|
|
220
|
-
* `.md` and `.mdx`: collected by Astro as local, not SVG or GIF, not inside `publicDir`, step enabled.
|
|
221
|
-
* Without a document location or a `publicDir`, the `public/` check is skipped.
|
|
222
|
-
*/
|
|
223
|
-
function pictureSource(value, astro, picture, documentPath) {
|
|
224
|
-
if (!picture.enabled)
|
|
225
|
-
return undefined;
|
|
226
|
-
const src = localImageSrc(value, astro);
|
|
227
|
-
if (src === undefined || VECTOR_OR_ANIMATED.test(withoutQueryOrFragment(src)))
|
|
228
|
-
return undefined;
|
|
229
|
-
const file = resolveLocalFile(src, documentPath);
|
|
230
|
-
if (picture.publicDir !== undefined && file !== undefined && isInsideDirectory(picture.publicDir, file)) {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
return src;
|
|
234
|
-
}
|
|
235
156
|
function pictureElement(img, src, index) {
|
|
236
157
|
const sources = PICTURE_FORMATS.map((format) => ({
|
|
237
158
|
type: 'element',
|
|
@@ -249,106 +170,6 @@ function pictureElement(img, src, index) {
|
|
|
249
170
|
}));
|
|
250
171
|
return { type: 'element', tagName: 'picture', properties: {}, children: [...sources, img] };
|
|
251
172
|
}
|
|
252
|
-
function estreeProgram(body) {
|
|
253
|
-
return { type: 'Program', sourceType: 'module', comments: [], body };
|
|
254
|
-
}
|
|
255
|
-
function jsxAttributeNode(attribute, flavor) {
|
|
256
|
-
if ('text' in attribute)
|
|
257
|
-
return { type: 'mdxJsxAttribute', name: attribute.name, value: attribute.text };
|
|
258
|
-
const expression = 'identifier' in attribute ? attribute.identifier : JSON.stringify(attribute.list);
|
|
259
|
-
const value = { type: 'mdxJsxAttributeValueExpression', value: expression };
|
|
260
|
-
if (flavor === 'estree') {
|
|
261
|
-
const node = 'identifier' in attribute
|
|
262
|
-
? { type: 'Identifier', name: attribute.identifier }
|
|
263
|
-
: {
|
|
264
|
-
type: 'ArrayExpression',
|
|
265
|
-
elements: attribute.list.map((item) => ({ type: 'Literal', value: item, raw: JSON.stringify(item) })),
|
|
266
|
-
};
|
|
267
|
-
value.data = { estree: estreeProgram([{ type: 'ExpressionStatement', expression: node }]) };
|
|
268
|
-
}
|
|
269
|
-
return { type: 'mdxJsxAttribute', name: attribute.name, value };
|
|
270
|
-
}
|
|
271
|
-
function importNode(flavor, text, declaration) {
|
|
272
|
-
if (flavor === 'source')
|
|
273
|
-
return { type: 'mdxjsEsm', value: text };
|
|
274
|
-
return { type: 'mdxjsEsm', value: '', data: { estree: estreeProgram([declaration]) } };
|
|
275
|
-
}
|
|
276
|
-
function newMdxPictureScope() {
|
|
277
|
-
return { assets: new Map(), component: false, pending: [] };
|
|
278
|
-
}
|
|
279
|
-
/** The identifier an asset import binds for `src`, queuing the imports the first time each is needed. */
|
|
280
|
-
function assetIdentifier(scope, src, flavor) {
|
|
281
|
-
const existing = scope.assets.get(src);
|
|
282
|
-
if (existing !== undefined)
|
|
283
|
-
return existing;
|
|
284
|
-
const name = `HagilightContentImage${scope.assets.size}`;
|
|
285
|
-
if (!scope.component) {
|
|
286
|
-
scope.component = true;
|
|
287
|
-
scope.pending.push(importNode(flavor, `import { Picture as ${PICTURE_COMPONENT} } from "${PICTURE_COMPONENT_SOURCE}";`, {
|
|
288
|
-
type: 'ImportDeclaration',
|
|
289
|
-
attributes: [],
|
|
290
|
-
source: { type: 'Literal', value: PICTURE_COMPONENT_SOURCE, raw: JSON.stringify(PICTURE_COMPONENT_SOURCE) },
|
|
291
|
-
specifiers: [{
|
|
292
|
-
type: 'ImportSpecifier',
|
|
293
|
-
imported: { type: 'Identifier', name: 'Picture' },
|
|
294
|
-
local: { type: 'Identifier', name: PICTURE_COMPONENT },
|
|
295
|
-
}],
|
|
296
|
-
}));
|
|
297
|
-
}
|
|
298
|
-
scope.pending.push(importNode(flavor, `import ${name} from ${JSON.stringify(src)};`, {
|
|
299
|
-
type: 'ImportDeclaration',
|
|
300
|
-
attributes: [],
|
|
301
|
-
source: { type: 'Literal', value: src, raw: JSON.stringify(src) },
|
|
302
|
-
specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
|
|
303
|
-
}));
|
|
304
|
-
scope.assets.set(src, name);
|
|
305
|
-
return name;
|
|
306
|
-
}
|
|
307
|
-
/** The image's properties after the baseline patch, with `alt` present, as `Picture` receives them. */
|
|
308
|
-
function pictureProperties(properties, options) {
|
|
309
|
-
const patch = hastImagePatch(properties, options);
|
|
310
|
-
const next = { ...properties };
|
|
311
|
-
if (patch.loading !== undefined)
|
|
312
|
-
next.loading = patch.loading;
|
|
313
|
-
if (patch.decoding !== undefined)
|
|
314
|
-
next.decoding = patch.decoding;
|
|
315
|
-
if (patch.classNames !== undefined)
|
|
316
|
-
next.className = patch.classNames;
|
|
317
|
-
if (next.alt === undefined || next.alt === null)
|
|
318
|
-
next.alt = '';
|
|
319
|
-
return next;
|
|
320
|
-
}
|
|
321
|
-
/** The `Picture` element an author would write by hand (D2), with the image's own attributes. */
|
|
322
|
-
function pictureComponent(properties, identifier, flavor) {
|
|
323
|
-
const attributes = [
|
|
324
|
-
{ name: 'src', identifier },
|
|
325
|
-
{ name: 'formats', list: [...PICTURE_FORMATS] },
|
|
326
|
-
{ name: 'fallbackFormat', text: PICTURE_FALLBACK_FORMAT },
|
|
327
|
-
];
|
|
328
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
329
|
-
if (key === 'src' || value === undefined || value === null || value === false)
|
|
330
|
-
continue;
|
|
331
|
-
const name = key === 'className' ? 'class' : key === 'htmlFor' ? 'for' : key;
|
|
332
|
-
attributes.push({ name, text: Array.isArray(value) ? value.map(String).join(' ') : String(value) });
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
type: 'mdxJsxFlowElement',
|
|
336
|
-
name: PICTURE_COMPONENT,
|
|
337
|
-
attributes: attributes.map((attribute) => jsxAttributeNode(attribute, flavor)),
|
|
338
|
-
children: [],
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
/** The root of the document, which holds the ESM imports a `Picture` needs. */
|
|
342
|
-
function documentRoot(node, ctx) {
|
|
343
|
-
let current = node;
|
|
344
|
-
for (let parent = ctx.parent(current); parent; parent = ctx.parent(current))
|
|
345
|
-
current = parent;
|
|
346
|
-
return current;
|
|
347
|
-
}
|
|
348
|
-
function mdxScope(data) {
|
|
349
|
-
data.hagilightContentPicture ??= newMdxPictureScope();
|
|
350
|
-
return data.hagilightContentPicture;
|
|
351
|
-
}
|
|
352
173
|
/** Sätteri hast plugin: Astro's default Markdown processor, through `hastPlugins`. */
|
|
353
174
|
export function satteriContentImages(options = {}, picture = { enabled: true }) {
|
|
354
175
|
const resolved = resolveContentImageOptions(options);
|
|
@@ -374,19 +195,6 @@ export function satteriContentImages(options = {}, picture = { enabled: true })
|
|
|
374
195
|
visit(node, ctx) {
|
|
375
196
|
if (isInsidePicture(node, ctx))
|
|
376
197
|
return;
|
|
377
|
-
const isDocument = ctx.sourceFormat === 'markdown' || ctx.sourceFormat === 'mdx';
|
|
378
|
-
const src = isDocument
|
|
379
|
-
? pictureSource(node.properties?.src, ctx.data.astro, picture, toFilePath(ctx.fileURL))
|
|
380
|
-
: undefined;
|
|
381
|
-
if (src !== undefined && ctx.sourceFormat === 'mdx') {
|
|
382
|
-
const root = documentRoot(node, ctx);
|
|
383
|
-
const scope = mdxScope(ctx.data);
|
|
384
|
-
const identifier = assetIdentifier(scope, src, 'source');
|
|
385
|
-
ctx.replaceNode(node, pictureComponent(pictureProperties(node.properties ?? {}, resolved), identifier, 'source'));
|
|
386
|
-
if (scope.pending.length > 0)
|
|
387
|
-
ctx.prependChild(root, scope.pending.splice(0));
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
198
|
const patch = hastImagePatch(node.properties ?? {}, resolved);
|
|
391
199
|
if (patch.loading !== undefined)
|
|
392
200
|
ctx.setProperty(node, 'loading', patch.loading);
|
|
@@ -394,8 +202,10 @@ export function satteriContentImages(options = {}, picture = { enabled: true })
|
|
|
394
202
|
ctx.setProperty(node, 'decoding', patch.decoding);
|
|
395
203
|
if (patch.classNames !== undefined)
|
|
396
204
|
ctx.setProperty(node, 'className', patch.classNames);
|
|
397
|
-
if (
|
|
398
|
-
|
|
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));
|
|
399
209
|
}
|
|
400
210
|
},
|
|
401
211
|
},
|
|
@@ -458,9 +268,6 @@ export function registerContentImages({ config, logger, enabled }) {
|
|
|
458
268
|
return;
|
|
459
269
|
}
|
|
460
270
|
const picture = { enabled: true };
|
|
461
|
-
const publicDir = toFilePath(config.publicDir);
|
|
462
|
-
if (publicDir !== undefined)
|
|
463
|
-
picture.publicDir = publicDir;
|
|
464
271
|
const plugin = processor.name === 'satteri'
|
|
465
272
|
? satteriContentImages({}, picture)
|
|
466
273
|
: (options) => rehypeContentImages(options, picture);
|
package/package.json
CHANGED