markdown-exit-s3-image 0.0.2 → 1.0.0

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { r as Options } from "./types-CsakQe1p.mjs";
1
+ import { n as Options } from "./types-DK7PR0JE.mjs";
2
2
  import { MarkdownExit } from "markdown-exit";
3
3
 
4
4
  //#region src/index.d.ts
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
- if (this.cache[key]) {
42
+ const decodedKey = decodeURIComponent(key);
43
+ if (this.cache[decodedKey]) {
45
44
  this.stats.cacheHits++;
46
- return this.cache[key];
45
+ return this.cache[decodedKey];
47
46
  }
48
47
  this.stats.apiRequests++;
49
48
  return null;
50
49
  }
51
50
  set(key, value) {
52
- if (JSON.stringify(this.cache[key]) !== JSON.stringify(value)) {
53
- this.cache[key] = value;
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
- lazy: {
85
- enable: true,
86
- skip_first: 2
87
- },
88
- supported_domains: [],
89
- ignore_formats: ["svg"],
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/index.ts
110
+ //#region src/bitiful.ts
111
111
  /**
112
- * Determine whether a remote URL matches the supported domain list.
113
- * Supports wildcards (*) for pattern matching.
112
+ * Bitiful CDN integration utilities
114
113
  */
115
- function isSupportedDomain(url, patterns) {
116
- if (patterns.length === 0) return true;
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 patterns.some((pattern) => {
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
- let cacheLoaded = false;
140
- let imageCount = 0;
141
- if (cache && !cacheLoaded) {
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 (!src.startsWith("http")) return imageRule(tokens, idx, info, env, self);
157
- if (!isSupportedDomain(src, options.supported_domains)) return imageRule(tokens, idx, info, env, self);
158
- if (shouldIgnoreFormat(src, options.ignore_formats)) return imageRule(tokens, idx, info, env, self);
159
- try {
160
- if (cache) {
161
- const cached = cache.get(src);
162
- if (cached) {
163
- imageCount++;
164
- return buildImageHTML(token.content, src, options, imageCount, cached.width, cached.height, cached.placeholder);
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, imageIndex, width, height, dataURL) {
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
- `sizes="${options.progressive.sizes || `(max-width: ${width}px) 100vw, ${width}px`}"`,
206
- shouldLazyLoad ? "loading=\"lazy\" decoding=\"async\"" : "fetchpriority=\"high\"",
207
- `style="width: 100%; height: auto; display: block; transition: opacity 0.4s; opacity: 0;"`,
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
- return `
211
- <div class="img-container" style="
212
- position: relative;
213
- overflow: hidden;
214
- aspect-ratio: ${width} / ${height};
215
- max-width: ${width}px;
216
- width: 100%;
217
- ${dataURL ? `background-image: url('${dataURL}');` : ""}
218
- background-size: cover;
219
- background-repeat: no-repeat;
220
- ">
221
- <img ${mainImgAttrs}>
222
- </div>
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 are eligible for processing. Supports wildcards.
48
- * When empty, all remote domains are processed.
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
- supported_domains?: string[];
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
- lazy: LazyOptions & {
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 a, ProgressiveOptions as i, LazyOptions as n, Options as r, CachePathOption as t };
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 { a as ResolvedOptions, i as ProgressiveOptions, n as LazyOptions, r as Options, t as CachePathOption } from "./types-CsakQe1p.mjs";
2
- export { CachePathOption, LazyOptions, Options, ProgressiveOptions, ResolvedOptions };
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
@@ -2,7 +2,7 @@
2
2
  "name": "markdown-exit-s3-image",
3
3
  "type": "module",
4
4
  "author": "gnix_aij",
5
- "version": "0.0.2",
5
+ "version": "1.0.0",
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",