markdown-exit-s3-image 0.0.1 → 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 +109 -86
- package/dist/{types-CsakQe1p.d.mts → types-DK7PR0JE.d.mts} +5 -30
- package/dist/types.d.mts +2 -2
- package/package.json +11 -8
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,8 +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
|
-
|
|
176
|
+
if (cache) cache.load().catch((err) => {
|
|
177
|
+
console.error("[ImageCache] Failed to load cache:", err);
|
|
178
|
+
});
|
|
141
179
|
process.once("beforeExit", () => {
|
|
142
180
|
if (cache) cache.save();
|
|
143
181
|
});
|
|
@@ -146,38 +184,28 @@ function image(md, userOptions) {
|
|
|
146
184
|
md.renderer.rules.image = async (tokens, idx, info, env, self) => {
|
|
147
185
|
const token = tokens[idx];
|
|
148
186
|
const src = token.attrGet("src");
|
|
149
|
-
if (!src) return imageRule(tokens, idx, info, env, self);
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!cacheLoaded && cache) {
|
|
154
|
-
await cache.load();
|
|
155
|
-
cacheLoaded = true;
|
|
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);
|
|
156
191
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const response = await fetch(src);
|
|
166
|
-
const image$1 = sharp(Buffer.from(await response.arrayBuffer()));
|
|
167
|
-
const { width, height } = await image$1.metadata();
|
|
168
|
-
const { data: thumbnailBuffer, info: thumbnailInfo } = await image$1.resize(100, 100, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
169
|
-
const placeholderUrl = rgbaToDataURL(thumbnailInfo.width, thumbnailInfo.height, new Uint8Array(thumbnailBuffer));
|
|
170
|
-
if (cache) cache.set(src, {
|
|
171
|
-
width,
|
|
172
|
-
height,
|
|
173
|
-
placeholder: placeholderUrl
|
|
174
|
-
});
|
|
175
|
-
imageCount++;
|
|
176
|
-
return buildImageHTML(token.content, src, options, imageCount, width, height, placeholderUrl);
|
|
177
|
-
} catch (e) {
|
|
178
|
-
console.error(`[markdown-exit-image] Error processing image ${src}:`, e);
|
|
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`);
|
|
179
198
|
return imageRule(tokens, idx, info, env, self);
|
|
180
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);
|
|
181
209
|
};
|
|
182
210
|
}
|
|
183
211
|
function generateSrcset(src, width, srcsetWidths) {
|
|
@@ -186,39 +214,34 @@ function generateSrcset(src, width, srcsetWidths) {
|
|
|
186
214
|
return `${w === width ? src : src.includes("?") ? `${src}&w=${w}` : `${src}?w=${w}`} ${w}w`;
|
|
187
215
|
}).join(", ");
|
|
188
216
|
}
|
|
189
|
-
function buildImageHTML(alt, src, options,
|
|
217
|
+
function buildImageHTML(alt, src, options, width, height, dataURL) {
|
|
190
218
|
const srcset = generateSrcset(src, width, options.progressive.srcset_widths);
|
|
191
|
-
const
|
|
192
|
-
/**
|
|
193
|
-
* 4. 返回包装后的 HTML
|
|
194
|
-
* - 外层 div 控制最大宽度并占用占位空间
|
|
195
|
-
* - aspect-ratio 确保容器高度在图片加载前就已确定
|
|
196
|
-
* - background-image 放置低分辨率模糊预览图 (dataURL)
|
|
197
|
-
*/
|
|
198
|
-
return `
|
|
199
|
-
<div class="img-container" style="
|
|
200
|
-
position: relative;
|
|
201
|
-
overflow: hidden;
|
|
202
|
-
aspect-ratio: ${width} / ${height};
|
|
203
|
-
max-width: ${width}px;
|
|
204
|
-
width: 100%;
|
|
205
|
-
background-image: url('${dataURL}');
|
|
206
|
-
background-size: cover;
|
|
207
|
-
background-repeat: no-repeat;
|
|
208
|
-
">
|
|
209
|
-
<img ${[
|
|
219
|
+
const mainImgAttrs = [
|
|
210
220
|
`src="${src}"`,
|
|
211
221
|
`alt="${alt}"`,
|
|
212
|
-
`width="${width}"`,
|
|
213
|
-
`height="${height}"`,
|
|
214
222
|
`srcset="${srcset}"`,
|
|
215
|
-
`
|
|
216
|
-
|
|
217
|
-
`style=
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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);"`
|
|
226
|
+
].filter(Boolean).join(" ");
|
|
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}>`;
|
|
222
245
|
}
|
|
223
246
|
|
|
224
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,9 +1,13 @@
|
|
|
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
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Efterklang/markdown-exit-s3-image.git"
|
|
10
|
+
},
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.mts",
|
|
@@ -24,18 +28,17 @@
|
|
|
24
28
|
"node": ">=20"
|
|
25
29
|
},
|
|
26
30
|
"scripts": {
|
|
27
|
-
"build": "tsdown"
|
|
31
|
+
"build": "tsdown",
|
|
32
|
+
"test": "bun run test/index.ts",
|
|
33
|
+
"test:clean": "rm -f test/cache.json test/output.html"
|
|
28
34
|
},
|
|
29
35
|
"peerDependencies": {
|
|
30
36
|
"markdown-exit": "1.0.0-beta.6"
|
|
31
37
|
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"sharp": "^0.34.5",
|
|
34
|
-
"thumbhash": "^0.1.1"
|
|
35
|
-
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@tsconfig/node20": "^20.1.8",
|
|
38
40
|
"@types/node": "^24.10.1",
|
|
39
|
-
"tsdown": "^0.16.6"
|
|
41
|
+
"tsdown": "^0.16.6",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
40
43
|
}
|
|
41
44
|
}
|