@ogify/core 0.1.4 → 0.1.5

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.js CHANGED
@@ -3,10 +3,37 @@
3
3
  var satori = require('satori');
4
4
  var satoriHtml = require('satori-html');
5
5
  var resvgJs = require('@resvg/resvg-js');
6
+ var lruCache = require('lru-cache');
7
+ var fs = require('fs');
8
+ var path = require('path');
9
+ var crypto = require('crypto');
10
+ var rtlCSSJS = require('rtl-css-js');
6
11
 
7
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
13
 
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () { return e[k]; }
24
+ });
25
+ }
26
+ });
27
+ }
28
+ n.default = e;
29
+ return Object.freeze(n);
30
+ }
31
+
9
32
  var satori__default = /*#__PURE__*/_interopDefault(satori);
33
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
34
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
35
+ var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
36
+ var rtlCSSJS__default = /*#__PURE__*/_interopDefault(rtlCSSJS);
10
37
 
11
38
  // src/template.ts
12
39
 
@@ -175,6 +202,216 @@ var GoogleFontDetector = class {
175
202
  }
176
203
  }
177
204
  };
205
+ var DEFAULT_TTL = 1e3 * 60 * 60 * 24 * 7;
206
+ var DEFAULT_MAX = 100;
207
+ var DEFAULT_CACHE_DIR = ".ogify-cache";
208
+ var CacheManager = class {
209
+ cache;
210
+ config;
211
+ cacheDir = DEFAULT_CACHE_DIR;
212
+ /**
213
+ * Creates a new CacheManager instance.
214
+ *
215
+ * @param config - Cache configuration (memory or filesystem)
216
+ */
217
+ constructor(config) {
218
+ this.config = config;
219
+ if (config.type === "memory") {
220
+ this.cache = new lruCache.LRUCache({
221
+ max: config.max || DEFAULT_MAX,
222
+ ttl: config.ttl || DEFAULT_TTL
223
+ });
224
+ }
225
+ if (config.type === "filesystem") {
226
+ this.cacheDir = config.dir || DEFAULT_CACHE_DIR;
227
+ this.ensureCacheDirectory();
228
+ this.cleanExpiredFiles().catch((err) => {
229
+ console.warn("Failed to clean expired cache files:", err);
230
+ });
231
+ }
232
+ }
233
+ // eslint-disable-next-line
234
+ generateKey(object) {
235
+ const sorted = Object.keys(object).sort().map((key) => `${key}=${object[key]}`);
236
+ return crypto__namespace.createHash("md5").update(sorted.join("|")).digest("hex");
237
+ }
238
+ /**
239
+ * Retrieves a value from the cache.
240
+ *
241
+ * For filesystem cache, attempts to load from disk if not in memory.
242
+ *
243
+ * @param key - Cache key
244
+ * @returns Cached buffer or undefined if not found
245
+ */
246
+ async get(key) {
247
+ if (this.config.type === "memory") {
248
+ return this.cache?.get(key);
249
+ }
250
+ if (this.config.type === "filesystem") {
251
+ return this.loadFromDisk(key);
252
+ }
253
+ return void 0;
254
+ }
255
+ /**
256
+ * Stores a value in the cache.
257
+ *
258
+ * For filesystem cache, also persists to disk.
259
+ *
260
+ * @param key - Cache key
261
+ * @param value - Buffer to cache
262
+ */
263
+ async set(key, value) {
264
+ if (this.config.type === "memory") {
265
+ this.cache?.set(key, value);
266
+ }
267
+ if (this.config.type === "filesystem") {
268
+ await this.saveToDisk(key, value);
269
+ }
270
+ }
271
+ /**
272
+ * Checks if a key exists in the cache.
273
+ *
274
+ * @param key - Cache key
275
+ * @returns true if the key exists and is not expired
276
+ */
277
+ async has(key) {
278
+ if (this.config.type === "memory") {
279
+ return this.cache?.has(key) || false;
280
+ }
281
+ if (this.config.type === "filesystem") {
282
+ try {
283
+ await fs__namespace.promises.access(path__namespace.join(this.cacheDir, this.getFilename(key)));
284
+ return true;
285
+ } catch {
286
+ return false;
287
+ }
288
+ }
289
+ return false;
290
+ }
291
+ /**
292
+ * Clears all cached items.
293
+ *
294
+ * For filesystem cache, also removes all files from disk.
295
+ */
296
+ async clear() {
297
+ if (this.config.type === "memory") {
298
+ this.cache?.clear();
299
+ }
300
+ if (this.config.type === "filesystem") {
301
+ await this.clearFilesystem();
302
+ }
303
+ }
304
+ /**
305
+ * Ensures the cache directory exists.
306
+ * Creates it if it doesn't exist.
307
+ */
308
+ ensureCacheDirectory() {
309
+ if (!this.cacheDir) return;
310
+ if (!fs__namespace.existsSync(this.cacheDir)) {
311
+ fs__namespace.mkdirSync(this.cacheDir, { recursive: true });
312
+ }
313
+ }
314
+ /**
315
+ * Generates a safe filename from a cache key.
316
+ *
317
+ * Uses SHA-256 hash to avoid filesystem issues with special characters.
318
+ *
319
+ * @param key - Cache key
320
+ * @returns Safe filename
321
+ */
322
+ getFilename(key) {
323
+ const hash = crypto__namespace.createHash("sha256").update(key).digest("hex");
324
+ return `${hash}.cache`;
325
+ }
326
+ /**
327
+ * Loads a cached item from disk.
328
+ *
329
+ * @param key - Cache key
330
+ * @returns Cached buffer or undefined if not found or expired
331
+ */
332
+ async loadFromDisk(key) {
333
+ if (this.config.type !== "filesystem") return void 0;
334
+ const filename = this.getFilename(key);
335
+ const filepath = path__namespace.join(this.cacheDir, filename);
336
+ try {
337
+ try {
338
+ await fs__namespace.promises.access(filepath);
339
+ } catch {
340
+ return void 0;
341
+ }
342
+ const stats = await fs__namespace.promises.stat(filepath);
343
+ const age = Date.now() - stats.mtimeMs;
344
+ const ttl = this.config.ttl || DEFAULT_TTL;
345
+ if (age > ttl) {
346
+ await fs__namespace.promises.unlink(filepath).catch(() => {
347
+ });
348
+ return void 0;
349
+ }
350
+ const data = await fs__namespace.promises.readFile(filepath);
351
+ this.cache?.set(key, data);
352
+ return data;
353
+ } catch (error) {
354
+ return void 0;
355
+ }
356
+ }
357
+ /**
358
+ * Saves a cached item to disk.
359
+ *
360
+ * @param key - Cache key
361
+ * @param value - Buffer to save
362
+ */
363
+ async saveToDisk(key, value) {
364
+ if (this.config.type !== "filesystem") return;
365
+ const filename = this.getFilename(key);
366
+ const filepath = path__namespace.join(this.cacheDir, filename);
367
+ try {
368
+ await fs__namespace.promises.writeFile(filepath, value);
369
+ } catch (error) {
370
+ }
371
+ }
372
+ /**
373
+ * Loads all cached items from filesystem into memory on initialization.
374
+ */
375
+ async cleanExpiredFiles() {
376
+ if (this.config.type !== "filesystem") return;
377
+ try {
378
+ const files = await fs__namespace.promises.readdir(this.cacheDir);
379
+ for (const file of files) {
380
+ if (!file.endsWith(".cache")) continue;
381
+ const filepath = path__namespace.join(this.cacheDir, file);
382
+ try {
383
+ const stats = await fs__namespace.promises.stat(filepath);
384
+ const age = Date.now() - stats.mtimeMs;
385
+ const ttl = this.config.ttl || DEFAULT_TTL;
386
+ if (age > ttl) {
387
+ await fs__namespace.promises.unlink(filepath).catch(() => {
388
+ });
389
+ }
390
+ } catch {
391
+ }
392
+ }
393
+ } catch (error) {
394
+ }
395
+ }
396
+ /**
397
+ * Clears all files from the filesystem cache directory.
398
+ */
399
+ async clearFilesystem() {
400
+ if (this.config.type !== "filesystem") return;
401
+ const cacheDir = this.cacheDir;
402
+ try {
403
+ const files = await fs__namespace.promises.readdir(cacheDir);
404
+ const unlinkPromises = files.map(async (file) => {
405
+ if (!file.endsWith(".cache")) return;
406
+ const filepath = path__namespace.join(cacheDir, file);
407
+ await fs__namespace.promises.unlink(filepath).catch(() => {
408
+ });
409
+ });
410
+ await Promise.all(unlinkPromises);
411
+ } catch (error) {
412
+ }
413
+ }
414
+ };
178
415
 
179
416
  // src/utils/emoji-loader.ts
180
417
  var ZERO_WIDTH_JOINER = String.fromCharCode(8205);
@@ -210,12 +447,15 @@ var apis = {
210
447
  fluent: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,
211
448
  fluentFlat: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`
212
449
  };
213
- var cache = {};
450
+ var cache = new CacheManager({
451
+ type: "memory"
452
+ });
214
453
  async function loadEmoji(type, text) {
215
454
  const code = getIconCode(text);
216
455
  const cacheKey = `${type}:${code}`;
217
- if (cacheKey in cache) {
218
- return cache[cacheKey];
456
+ const cached = await cache.get(cacheKey);
457
+ if (cached) {
458
+ return cached.toString();
219
459
  }
220
460
  if (!type || !apis[type]) {
221
461
  type = "noto";
@@ -224,19 +464,22 @@ async function loadEmoji(type, text) {
224
464
  const baseUrl = typeof api === "function" ? api(code) : api;
225
465
  const fullUrl = typeof api === "function" ? baseUrl : `${baseUrl}${code.toUpperCase()}.svg`;
226
466
  const emojiPromise = fetch(fullUrl).then((response) => response.text()).then((svgContent) => `data:image/svg+xml;base64,${btoa(svgContent)}`);
227
- cache[cacheKey] = emojiPromise;
467
+ await cache.set(cacheKey, Buffer.from(await emojiPromise));
228
468
  return emojiPromise;
229
469
  }
230
470
 
231
- // src/utils/fetcher.ts
232
- var cache2 = {};
471
+ // src/utils/font-fetcher.ts
472
+ var cache2 = new CacheManager({
473
+ type: "memory"
474
+ });
233
475
  var loadFontFromUrl = async (url) => {
234
- if (url in cache2) {
235
- return cache2[url];
476
+ const cachedFont = await cache2.get(url);
477
+ if (cachedFont) {
478
+ return cachedFont;
236
479
  }
237
- const fontData = fetch(url).then((response) => response.arrayBuffer());
238
- cache2[url] = fontData;
239
- return fontData;
480
+ const fontData = await fetch(url).then((response) => response.arrayBuffer());
481
+ await cache2.set(url, Buffer.from(fontData));
482
+ return Buffer.from(fontData);
240
483
  };
241
484
 
242
485
  // src/utils/additional-asset-loader.ts
@@ -314,11 +557,12 @@ var DEFAULT_HEIGHT = 630;
314
557
  async function renderTemplate(template, params, options) {
315
558
  const width = options?.width || DEFAULT_WIDTH;
316
559
  const height = options?.height || DEFAULT_HEIGHT;
317
- const satoriFonts = await loadFonts(template.fonts);
560
+ const fonts = options?.fonts?.length ? options.fonts : template.fonts;
561
+ const emojiProvider = options?.emojiProvider || template.emojiProvider || "noto";
562
+ const satoriFonts = await loadFonts(fonts);
318
563
  const htmlString = await template.renderer({
319
564
  params: typeof params === "function" ? await params() : params,
320
- width,
321
- height
565
+ ...options
322
566
  });
323
567
  const element = satoriHtml.html(htmlString);
324
568
  const svg = await satori__default.default(element, {
@@ -337,32 +581,25 @@ async function renderTemplate(template, params, options) {
337
581
  // Asset type ('emoji' or other)
338
582
  segment,
339
583
  // The character(s) to load
340
- fonts: template.fonts,
584
+ fonts,
341
585
  // Available fonts for fallback detection
342
- emojiProvider: template.emojiProvider || "noto"
586
+ emojiProvider
343
587
  // Emoji provider (default: noto)
344
588
  });
345
589
  }
346
590
  });
347
- const resvg = new resvgJs.Resvg(svg, {
591
+ const pngData = await resvgJs.renderAsync(svg, {
348
592
  fitTo: {
349
593
  mode: "width",
350
594
  // Scale based on width, maintain aspect ratio
351
595
  value: width
352
596
  }
353
597
  });
354
- const pngData = resvg.render();
355
598
  return Buffer.from(pngData.asPng());
356
599
  }
357
600
 
358
601
  // src/renderer.ts
359
602
  function validateTemplate(config) {
360
- if (!config.id) {
361
- throw new Error("Template must have an id");
362
- }
363
- if (!config.name) {
364
- throw new Error("Template must have a name");
365
- }
366
603
  if (typeof config.renderer !== "function") {
367
604
  throw new Error("Template must have a renderer function");
368
605
  }
@@ -372,9 +609,14 @@ function defineTemplate(config) {
372
609
  validateTemplate(config);
373
610
  return config;
374
611
  }
612
+ var DEFAULT_CACHE = {
613
+ type: "memory"
614
+ };
375
615
  var TemplateRenderer = class {
376
616
  /** Configuration including global settings and lifecycle hooks */
377
617
  config;
618
+ /** Cache manager for fonts and icons */
619
+ cacheManager;
378
620
  /**
379
621
  * Internal registry mapping template IDs to template definitions.
380
622
  *
@@ -383,7 +625,7 @@ var TemplateRenderer = class {
383
625
  * - Guaranteed insertion order
384
626
  * - Better memory efficiency than objects
385
627
  */
386
- templates = /* @__PURE__ */ new Map();
628
+ templates = {};
387
629
  /**
388
630
  * Creates a new TemplateRenderer instance.
389
631
  *
@@ -391,6 +633,7 @@ var TemplateRenderer = class {
391
633
  */
392
634
  constructor(config) {
393
635
  this.config = config;
636
+ this.cacheManager = new CacheManager(config.cache || DEFAULT_CACHE);
394
637
  this.registerTemplates(config.templates);
395
638
  }
396
639
  /**
@@ -399,12 +642,13 @@ var TemplateRenderer = class {
399
642
  * Templates are indexed by their ID for fast lookup.
400
643
  * If a template with the same ID already exists, it will be overwritten.
401
644
  *
402
- * @param templates - Array of template definitions to register
645
+ * @param templates - Map of template definitions to register
403
646
  */
404
647
  registerTemplates(templates) {
405
- for (const template of templates) {
406
- this.templates.set(template.id, template);
407
- }
648
+ const keys = Object.keys(templates);
649
+ keys.forEach((key) => {
650
+ this.templates[key] = templates[key];
651
+ });
408
652
  }
409
653
  /**
410
654
  * Retrieves a template by its unique ID.
@@ -418,20 +662,7 @@ var TemplateRenderer = class {
418
662
  * @returns The template definition, or undefined if not found
419
663
  */
420
664
  getTemplate(id) {
421
- return this.templates.get(id);
422
- }
423
- /**
424
- * Gets all registered template IDs.
425
- *
426
- * Useful for:
427
- * - Building template selection dropdowns
428
- * - Listing available templates in documentation
429
- * - Debugging template registration
430
- *
431
- * @returns Array of template IDs
432
- */
433
- getTemplateIds() {
434
- return Array.from(this.templates.keys());
665
+ return this.templates[id];
435
666
  }
436
667
  /**
437
668
  * Renders a template to a PNG image buffer.
@@ -454,7 +685,7 @@ var TemplateRenderer = class {
454
685
  * - Throws if rendering fails (font loading, HTML generation, etc.)
455
686
  *
456
687
  * @param templateId - ID of the template to render
457
- * @param params - Parameters to pass to the template
688
+ * @param params - Parameters (or function returning params) to pass to the template
458
689
  * @param options - Optional rendering options
459
690
  * @param options.width - Custom image width in pixels (default: 1200)
460
691
  * @param options.height - Custom image height in pixels (default: 630)
@@ -462,33 +693,69 @@ var TemplateRenderer = class {
462
693
  * @throws Error if template is not found or rendering fails
463
694
  */
464
695
  async renderToImage(templateId, params, options) {
465
- const { defaultParams } = this.config;
696
+ const { sharedParams } = this.config;
466
697
  const template = this.getTemplate(templateId);
467
698
  if (!template) {
468
699
  throw new Error(`Template '${templateId}' not found`);
469
700
  }
470
701
  const mergedParams = {
471
- ...typeof defaultParams === "function" ? await defaultParams() : defaultParams,
472
- ...typeof params === "function" ? await params() : params
702
+ ...typeof sharedParams === "function" ? await sharedParams() : sharedParams || {},
703
+ ...typeof params === "function" ? await params() : params || {}
473
704
  };
705
+ const cacheKey = this.cacheManager.generateKey({
706
+ templateId,
707
+ ...mergedParams,
708
+ ...options
709
+ });
710
+ const cached = await this.cacheManager.get(cacheKey);
711
+ if (cached) {
712
+ return cached;
713
+ }
474
714
  if (this.config.beforeRender) {
475
715
  await this.config.beforeRender(templateId, mergedParams);
476
716
  }
477
717
  const imageBuffer = await renderTemplate(template, mergedParams, options);
718
+ await this.cacheManager.set(cacheKey, imageBuffer);
478
719
  if (this.config.afterRender) {
479
720
  await this.config.afterRender(templateId, mergedParams, imageBuffer);
480
721
  }
481
722
  return imageBuffer;
482
723
  }
483
724
  };
484
- function createTemplateRenderer(config) {
725
+ function createRenderer(config) {
485
726
  return new TemplateRenderer(config);
486
727
  }
487
- /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
728
+
729
+ // src/utils/clsx.ts
730
+ function clsx(...inputs) {
731
+ var i = 0, tmp, str = "", len = inputs.length;
732
+ for (; i < len; i++) {
733
+ if (tmp = inputs[i]) {
734
+ if (typeof tmp === "string") {
735
+ str += (str && " ") + tmp;
736
+ }
737
+ }
738
+ }
739
+ return str;
740
+ }
741
+ var objectToStyle = (style, options) => {
742
+ if (!style) {
743
+ return "";
744
+ }
745
+ const { isRTL = false } = options || {};
746
+ return Object.entries(isRTL ? rtlCSSJS__default.default(style) : style).filter(([_, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => {
747
+ if (value || value === 0) {
748
+ const cssKey = key.startsWith("--") ? key : key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
749
+ return `${cssKey}:${value}`;
750
+ }
751
+ return "";
752
+ }).filter(Boolean).join(";");
753
+ };
488
754
 
489
755
  exports.TemplateRenderer = TemplateRenderer;
490
- exports.createTemplateRenderer = createTemplateRenderer;
756
+ exports.clsx = clsx;
757
+ exports.createRenderer = createRenderer;
491
758
  exports.defineTemplate = defineTemplate;
492
- exports.loadFontFromUrl = loadFontFromUrl;
759
+ exports.objectToStyle = objectToStyle;
493
760
  exports.renderTemplate = renderTemplate;
494
761
  exports.validateTemplate = validateTemplate;