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 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,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
- let cacheLoaded = false;
140
- let imageCount = 0;
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 (!src.startsWith("http")) return imageRule(tokens, idx, info, env, self);
151
- if (!isSupportedDomain(src, options.supported_domains)) return imageRule(tokens, idx, info, env, self);
152
- if (shouldIgnoreFormat(src, options.ignore_formats)) return imageRule(tokens, idx, info, env, self);
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
- try {
158
- if (cache) {
159
- const cached = cache.get(src);
160
- if (cached) {
161
- imageCount++;
162
- return buildImageHTML(token.content, src, options, imageCount, cached.width, cached.height, cached.placeholder);
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, imageIndex, width, height, dataURL) {
217
+ function buildImageHTML(alt, src, options, width, height, dataURL) {
190
218
  const srcset = generateSrcset(src, width, options.progressive.srcset_widths);
191
- const shouldLazyLoad = options.lazy.enable && imageIndex > options.lazy.skip_first;
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
- `sizes="${options.progressive.sizes || `(max-width: ${width}px) 100vw, ${width}px`}"`,
216
- shouldLazyLoad ? "loading=\"lazy\" decoding=\"async\"" : "fetchpriority=\"high\"",
217
- `style="width: 100%; height: auto; display: block; transition: opacity 0.4s; opacity: 0;"`,
218
- `onload="this.style.opacity=1; this.parentElement.style.backgroundImage='none';"`
219
- ].filter(Boolean).join(" ")}>
220
- </div>
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 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
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "markdown-exit-s3-image",
3
3
  "type": "module",
4
- "author": "gnix_aij",
5
- "version": "0.0.1",
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
  }