markdown-exit-s3-image 0.0.2 → 0.0.3
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/dist/index.d.mts +1 -1
- package/dist/index.mjs +108 -87
- package/dist/{types-CsakQe1p.d.mts → types-DK7PR0JE.d.mts} +5 -30
- package/dist/types.d.mts +2 -2
- package/package.json +5 -7
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import sharp from "sharp";
|
|
2
|
-
import { rgbaToDataURL } from "thumbhash";
|
|
3
1
|
import { promises } from "node:fs";
|
|
4
2
|
|
|
5
3
|
//#region src/cache.ts
|
|
@@ -41,16 +39,18 @@ var ImageCache = class {
|
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
get(key) {
|
|
44
|
-
|
|
42
|
+
const decodedKey = decodeURIComponent(key);
|
|
43
|
+
if (this.cache[decodedKey]) {
|
|
45
44
|
this.stats.cacheHits++;
|
|
46
|
-
return this.cache[
|
|
45
|
+
return this.cache[decodedKey];
|
|
47
46
|
}
|
|
48
47
|
this.stats.apiRequests++;
|
|
49
48
|
return null;
|
|
50
49
|
}
|
|
51
50
|
set(key, value) {
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const decodedKey = decodeURIComponent(key);
|
|
52
|
+
if (JSON.stringify(this.cache[decodedKey]) !== JSON.stringify(value)) {
|
|
53
|
+
this.cache[decodedKey] = value;
|
|
54
54
|
this.isDirty = true;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -81,12 +81,16 @@ function resolveOptions(userOptions = {}) {
|
|
|
81
81
|
3e3
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
ignore_formats: [
|
|
85
|
+
"svg",
|
|
86
|
+
"gif",
|
|
87
|
+
"webm"
|
|
88
|
+
],
|
|
89
|
+
bitiful_domains: [
|
|
90
|
+
"assets.vluv.space",
|
|
91
|
+
"s3.bitiful.net",
|
|
92
|
+
"bitiful.com"
|
|
93
|
+
],
|
|
90
94
|
cache_path: null
|
|
91
95
|
};
|
|
92
96
|
return {
|
|
@@ -96,35 +100,68 @@ function resolveOptions(userOptions = {}) {
|
|
|
96
100
|
...defaultOptions.progressive,
|
|
97
101
|
...userOptions.progressive
|
|
98
102
|
},
|
|
99
|
-
lazy: {
|
|
100
|
-
...defaultOptions.lazy,
|
|
101
|
-
...userOptions.lazy
|
|
102
|
-
},
|
|
103
|
-
supported_domains: userOptions.supported_domains ?? defaultOptions.supported_domains,
|
|
104
103
|
ignore_formats: userOptions.ignore_formats ?? defaultOptions.ignore_formats,
|
|
104
|
+
bitiful_domains: userOptions.bitiful_domains ?? defaultOptions.bitiful_domains,
|
|
105
105
|
cache_path: userOptions.cache_path ?? defaultOptions.cache_path
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
//#endregion
|
|
110
|
-
//#region src/
|
|
110
|
+
//#region src/bitiful.ts
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
* Supports wildcards (*) for pattern matching.
|
|
112
|
+
* Bitiful CDN integration utilities
|
|
114
113
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Check if URL belongs to Bitiful CDN.
|
|
116
|
+
*/
|
|
117
|
+
function isBitifulDomain(url, bitifulDomains) {
|
|
117
118
|
try {
|
|
118
119
|
const hostname = new URL(url).hostname;
|
|
119
|
-
return
|
|
120
|
-
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
|
121
|
-
return new RegExp(`^${regexPattern}$`, "i").test(hostname);
|
|
122
|
-
});
|
|
120
|
+
return bitifulDomains.some((domain) => hostname.includes(domain));
|
|
123
121
|
} catch {
|
|
124
122
|
return false;
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
/**
|
|
126
|
+
* Fetch dimension info from Bitiful API.
|
|
127
|
+
*/
|
|
128
|
+
async function getBitifulDimension(imageUrl) {
|
|
129
|
+
try {
|
|
130
|
+
const baseUrl = imageUrl.split("?")[0];
|
|
131
|
+
const response = await fetch(`${baseUrl}?fmt=info`);
|
|
132
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
133
|
+
const { ImageWidth: width, ImageHeight: height } = await response.json();
|
|
134
|
+
if (typeof width === "number" && typeof height === "number") return {
|
|
135
|
+
width,
|
|
136
|
+
height
|
|
137
|
+
};
|
|
138
|
+
return null;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.warn(`[Bitiful] Dimension error for ${imageUrl}:`, error instanceof Error ? error.message : String(error));
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Fetch thumbhash data URL from Bitiful API.
|
|
146
|
+
*/
|
|
147
|
+
async function getBitifulThumbhash(imageUrl) {
|
|
148
|
+
try {
|
|
149
|
+
const baseUrl = imageUrl.split("?")[0];
|
|
150
|
+
const response = await fetch(`${baseUrl}?fmt=thumbhash`);
|
|
151
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
152
|
+
const base64String = await response.text();
|
|
153
|
+
const thumbhashBytes = new Uint8Array(Buffer.from(base64String.trim(), "base64"));
|
|
154
|
+
const { thumbHashToDataURL } = await import("thumbhash");
|
|
155
|
+
return thumbHashToDataURL(thumbhashBytes);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.warn(`[Bitiful] Thumbhash error for ${imageUrl}:`, error instanceof Error ? error.message : String(error));
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/index.ts
|
|
164
|
+
/**
|
|
128
165
|
* Check if file format should be ignored based on URL extension.
|
|
129
166
|
*/
|
|
130
167
|
function shouldIgnoreFormat(url, formats) {
|
|
@@ -136,14 +173,9 @@ function image(md, userOptions) {
|
|
|
136
173
|
if (!options.progressive.enable) return;
|
|
137
174
|
const cachePath = options.cache_path;
|
|
138
175
|
const cache = cachePath ? new ImageCache(cachePath) : null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
cache.load().catch((err) => {
|
|
143
|
-
console.error("[ImageCache] Failed to load cache:", err);
|
|
144
|
-
});
|
|
145
|
-
cacheLoaded = true;
|
|
146
|
-
}
|
|
176
|
+
if (cache) cache.load().catch((err) => {
|
|
177
|
+
console.error("[ImageCache] Failed to load cache:", err);
|
|
178
|
+
});
|
|
147
179
|
process.once("beforeExit", () => {
|
|
148
180
|
if (cache) cache.save();
|
|
149
181
|
});
|
|
@@ -152,39 +184,28 @@ function image(md, userOptions) {
|
|
|
152
184
|
md.renderer.rules.image = async (tokens, idx, info, env, self) => {
|
|
153
185
|
const token = tokens[idx];
|
|
154
186
|
const src = token.attrGet("src");
|
|
155
|
-
if (!src) return imageRule(tokens, idx, info, env, self);
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
const response = await fetch(src);
|
|
168
|
-
const image$1 = sharp(Buffer.from(await response.arrayBuffer()));
|
|
169
|
-
const { width, height } = await image$1.metadata();
|
|
170
|
-
let placeholderUrl = "";
|
|
171
|
-
try {
|
|
172
|
-
const { data: thumbnailBuffer, info: thumbnailInfo } = await image$1.resize(100, 100, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
173
|
-
placeholderUrl = rgbaToDataURL(thumbnailInfo.width, thumbnailInfo.height, new Uint8Array(thumbnailBuffer));
|
|
174
|
-
} catch (thumbErr) {
|
|
175
|
-
console.warn(`[ImageCache] Failed to generate placeholder for ${src}: ${thumbErr instanceof Error ? thumbErr.message : String(thumbErr)}`);
|
|
176
|
-
}
|
|
177
|
-
if (cache && placeholderUrl) cache.set(src, {
|
|
178
|
-
width,
|
|
179
|
-
height,
|
|
180
|
-
placeholder: placeholderUrl
|
|
181
|
-
});
|
|
182
|
-
imageCount++;
|
|
183
|
-
return buildImageHTML(token.content, src, options, imageCount, width, height, placeholderUrl);
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.error(`[markdown-exit-image] Error processing image ${src}:`, e);
|
|
187
|
+
if (!src || !src.startsWith("http") || !isBitifulDomain(src, options.bitiful_domains) || shouldIgnoreFormat(src, options.ignore_formats) || process.env.NODE_ENV == "development") return imageRule(tokens, idx, info, env, self);
|
|
188
|
+
if (cache) {
|
|
189
|
+
const cached = cache.get(src);
|
|
190
|
+
if (cached) return buildImageHTML(token.content, src, options, cached.width, cached.height, cached.dataURL);
|
|
191
|
+
}
|
|
192
|
+
let width;
|
|
193
|
+
let height;
|
|
194
|
+
let placeholderUrl = "";
|
|
195
|
+
const [dimensionResult, thumbhashResult] = await Promise.all([getBitifulDimension(src), getBitifulThumbhash(src)]);
|
|
196
|
+
if (!dimensionResult) {
|
|
197
|
+
console.warn(`[ImagePlugin] Skipping progressive image for ${src} - dimensions unavailable`);
|
|
186
198
|
return imageRule(tokens, idx, info, env, self);
|
|
187
199
|
}
|
|
200
|
+
width = dimensionResult.width;
|
|
201
|
+
height = dimensionResult.height;
|
|
202
|
+
placeholderUrl = thumbhashResult || "";
|
|
203
|
+
if (cache && placeholderUrl) cache.set(src, {
|
|
204
|
+
width,
|
|
205
|
+
height,
|
|
206
|
+
dataURL: placeholderUrl
|
|
207
|
+
});
|
|
208
|
+
return buildImageHTML(token.content, src, options, width, height, placeholderUrl);
|
|
188
209
|
};
|
|
189
210
|
}
|
|
190
211
|
function generateSrcset(src, width, srcsetWidths) {
|
|
@@ -193,34 +214,34 @@ function generateSrcset(src, width, srcsetWidths) {
|
|
|
193
214
|
return `${w === width ? src : src.includes("?") ? `${src}&w=${w}` : `${src}?w=${w}`} ${w}w`;
|
|
194
215
|
}).join(", ");
|
|
195
216
|
}
|
|
196
|
-
function buildImageHTML(alt, src, options,
|
|
217
|
+
function buildImageHTML(alt, src, options, width, height, dataURL) {
|
|
197
218
|
const srcset = generateSrcset(src, width, options.progressive.srcset_widths);
|
|
198
|
-
const shouldLazyLoad = options.lazy.enable && imageIndex > options.lazy.skip_first;
|
|
199
219
|
const mainImgAttrs = [
|
|
200
220
|
`src="${src}"`,
|
|
201
221
|
`alt="${alt}"`,
|
|
202
|
-
`width="${width}"`,
|
|
203
|
-
`height="${height}"`,
|
|
204
222
|
`srcset="${srcset}"`,
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
`style=
|
|
208
|
-
`onload="this.style.opacity=1; this.parentElement.style.backgroundImage='none';"`
|
|
223
|
+
`loading="lazy" decoding="async"`,
|
|
224
|
+
`style="width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.6s ease-in-out;"`,
|
|
225
|
+
`onload="this.style.opacity=1; setTimeout(() => { this.parentElement.style.backgroundImage='none'; }, 600);"`
|
|
209
226
|
].filter(Boolean).join(" ");
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
/**
|
|
228
|
+
* 3. 返回包装后的 HTML
|
|
229
|
+
* - 只有当有 dataURL 时才包装 div
|
|
230
|
+
* - 外层 div 设置背景和宽高比
|
|
231
|
+
*/
|
|
232
|
+
if (dataURL) return `<div class="pic" style="
|
|
233
|
+
position: relative;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
width: 100%;
|
|
236
|
+
max-width: ${width}px;
|
|
237
|
+
background-image: url('${dataURL}');
|
|
238
|
+
background-size: cover;
|
|
239
|
+
background-repeat: no-repeat;
|
|
240
|
+
aspect-ratio: ${width} / ${height};
|
|
241
|
+
">
|
|
242
|
+
<img ${mainImgAttrs}>
|
|
243
|
+
</div>`;
|
|
244
|
+
else return `<img ${mainImgAttrs}>`;
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
//#endregion
|
|
@@ -20,21 +20,6 @@ interface ProgressiveOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
sizes?: string;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Options for controlling lazy loading behaviour.
|
|
25
|
-
*/
|
|
26
|
-
interface LazyOptions {
|
|
27
|
-
/**
|
|
28
|
-
* Enable or disable lazy loading.
|
|
29
|
-
* @default true
|
|
30
|
-
*/
|
|
31
|
-
enable?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Number of images to skip before applying lazy loading.
|
|
34
|
-
* @default 2
|
|
35
|
-
*/
|
|
36
|
-
skip_first?: number;
|
|
37
|
-
}
|
|
38
23
|
/**
|
|
39
24
|
* Configuration options consumed by the plugin.
|
|
40
25
|
*/
|
|
@@ -44,22 +29,16 @@ interface Options {
|
|
|
44
29
|
*/
|
|
45
30
|
progressive?: ProgressiveOptions;
|
|
46
31
|
/**
|
|
47
|
-
* Domains that
|
|
48
|
-
*
|
|
49
|
-
* @default []
|
|
50
|
-
* @example ["example.com", "*.cdn.example.com"]
|
|
32
|
+
* Domains that support Bitiful API for thumbhash and info.
|
|
33
|
+
* @default ["assets.vluv.space", "s3.bitiful.net", "bitiful.com"]
|
|
51
34
|
*/
|
|
52
|
-
|
|
35
|
+
bitiful_domains?: string[];
|
|
53
36
|
/**
|
|
54
37
|
* Array of file format extensions to ignore (case-insensitive).
|
|
55
38
|
* @default ["svg"]
|
|
56
39
|
* @example ["svg", "gif"]
|
|
57
40
|
*/
|
|
58
41
|
ignore_formats?: string[];
|
|
59
|
-
/**
|
|
60
|
-
* Options for lazy loading.
|
|
61
|
-
*/
|
|
62
|
-
lazy?: LazyOptions;
|
|
63
42
|
/**
|
|
64
43
|
* Path to the cache file. When unset, caching is disabled.
|
|
65
44
|
* @default null
|
|
@@ -75,13 +54,9 @@ interface ResolvedOptions extends Options {
|
|
|
75
54
|
enable: boolean;
|
|
76
55
|
srcset_widths: number[];
|
|
77
56
|
};
|
|
78
|
-
|
|
79
|
-
enable: boolean;
|
|
80
|
-
skip_first: number;
|
|
81
|
-
};
|
|
82
|
-
supported_domains: string[];
|
|
57
|
+
bitiful_domains: string[];
|
|
83
58
|
ignore_formats: string[];
|
|
84
59
|
cache_path: CachePathOption;
|
|
85
60
|
}
|
|
86
61
|
//#endregion
|
|
87
|
-
export { ResolvedOptions as
|
|
62
|
+
export { ResolvedOptions as i, Options as n, ProgressiveOptions as r, CachePathOption as t };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { CachePathOption,
|
|
1
|
+
import { i as ResolvedOptions, n as Options, r as ProgressiveOptions, t as CachePathOption } from "./types-DK7PR0JE.mjs";
|
|
2
|
+
export { CachePathOption, Options, ProgressiveOptions, ResolvedOptions };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdown-exit-s3-image",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"author": "
|
|
5
|
-
"version": "0.0.
|
|
4
|
+
"author": "GnixAij",
|
|
5
|
+
"version": "0.0.3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -28,15 +28,13 @@
|
|
|
28
28
|
"node": ">=20"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsdown"
|
|
31
|
+
"build": "tsdown",
|
|
32
|
+
"test": "bun run test/index.ts",
|
|
33
|
+
"test:clean": "rm -f test/cache.json test/output.html"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"markdown-exit": "1.0.0-beta.6"
|
|
35
37
|
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"sharp": "^0.34.5",
|
|
38
|
-
"thumbhash": "^0.1.1"
|
|
39
|
-
},
|
|
40
38
|
"devDependencies": {
|
|
41
39
|
"@tsconfig/node20": "^20.1.8",
|
|
42
40
|
"@types/node": "^24.10.1",
|