@heyramzi/utils 1.12.0 → 1.14.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.
Files changed (47) hide show
  1. package/dist/ai/image.js +220 -0
  2. package/dist/ai/models.js +261 -0
  3. package/dist/analytics/client.js +57 -0
  4. package/dist/analytics/server.js +54 -0
  5. package/dist/analytics/svelte.js +24 -0
  6. package/dist/analytics/types.js +0 -0
  7. package/dist/analytics/umami/protocol.js +35 -0
  8. package/dist/analytics/umami/reporting.js +189 -0
  9. package/dist/auth/config/otp.js +4 -0
  10. package/dist/auth/config/rate-limits.js +20 -0
  11. package/dist/auth/config/trusted-origins.js +15 -0
  12. package/dist/auth/email/index.js +3 -0
  13. package/dist/auth/email/layout.js +84 -0
  14. package/dist/auth/email/resend.js +30 -0
  15. package/dist/auth/email/template.js +57 -0
  16. package/dist/auth/email/types.js +0 -0
  17. package/dist/auth/index.js +3 -0
  18. package/dist/cache.js +4 -0
  19. package/dist/cn.js +5 -0
  20. package/dist/collection.js +8 -0
  21. package/dist/crypto.js +117 -0
  22. package/dist/error.js +15 -0
  23. package/dist/errors/sentry/index.js +15 -0
  24. package/dist/errors/sentry/protocol.js +130 -0
  25. package/dist/errors/sentry/sveltekit.js +145 -0
  26. package/dist/format.js +62 -0
  27. package/dist/grab.js +112 -0
  28. package/dist/index.js +10 -0
  29. package/dist/logger-types.js +0 -0
  30. package/dist/logger.js +22 -0
  31. package/dist/math.js +3 -0
  32. package/dist/og/fonts.js +46 -0
  33. package/dist/og/index.js +3 -0
  34. package/dist/og/markup.js +31 -0
  35. package/dist/og/renderer.js +29 -0
  36. package/dist/og/types.js +0 -0
  37. package/dist/result.js +26 -0
  38. package/dist/string.js +32 -0
  39. package/dist/testimonials/render-content.js +15 -0
  40. package/dist/testimonials/server.js +21 -0
  41. package/dist/testimonials/types.js +0 -0
  42. package/dist/timing.js +20 -0
  43. package/dist/vite.js +265 -0
  44. package/package.json +129 -32
  45. package/src/auth/email/template.ts +2 -2
  46. package/src/errors/sentry/sveltekit.test.ts +63 -2
  47. package/src/errors/sentry/sveltekit.ts +43 -5
@@ -0,0 +1,130 @@
1
+ export function parseDsn(dsn) {
2
+ const m = /^(https?):\/\/([^@/]+)@([^/]+)\/(?:.*\/)?(\d+)\/?$/.exec(dsn.trim());
3
+ const [, scheme, publicKey, host, projectId] = m ?? [];
4
+ if (!scheme || !publicKey || !host || !projectId) return null;
5
+ return {
6
+ publicKey,
7
+ origin: `${scheme}://${host}`,
8
+ projectId
9
+ };
10
+ }
11
+ function randomHex32() {
12
+ let out = "";
13
+ for(let i = 0; i < 32; i++)out += Math.floor(Math.random() * 16).toString(16);
14
+ return out;
15
+ }
16
+ const V8_FRAME = /^\s*at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?\s*$/;
17
+ export function parseStack(stack) {
18
+ if (!stack) return [];
19
+ const frames = [];
20
+ for (const line of stack.split("\n")){
21
+ const m = V8_FRAME.exec(line);
22
+ if (!m) continue;
23
+ const frame = {
24
+ filename: m[2],
25
+ lineno: Number(m[3]),
26
+ colno: Number(m[4])
27
+ };
28
+ if (m[1]) frame.function = m[1];
29
+ frames.push(frame);
30
+ }
31
+ return frames.reverse();
32
+ }
33
+ function describe(error) {
34
+ if (error instanceof Error) {
35
+ return {
36
+ type: error.name || "Error",
37
+ value: error.message,
38
+ stack: error.stack
39
+ };
40
+ }
41
+ if (typeof error === "string") return {
42
+ type: "Error",
43
+ value: error
44
+ };
45
+ try {
46
+ return {
47
+ type: "Error",
48
+ value: JSON.stringify(error)
49
+ };
50
+ } catch {
51
+ return {
52
+ type: "Error",
53
+ value: String(error)
54
+ };
55
+ }
56
+ }
57
+ const MAX_CHAIN = 5;
58
+ function describeChain(error) {
59
+ const chain = [
60
+ describe(error)
61
+ ];
62
+ const seen = new Set([
63
+ error
64
+ ]);
65
+ let next = error instanceof Error ? error.cause : undefined;
66
+ while(next !== undefined && next !== null && !seen.has(next) && chain.length < MAX_CHAIN){
67
+ seen.add(next);
68
+ chain.push(describe(next));
69
+ next = next instanceof Error ? next.cause : undefined;
70
+ }
71
+ return chain.reverse();
72
+ }
73
+ function cleanTags(input) {
74
+ const tags = {};
75
+ for (const [k, v] of Object.entries(input.tags ?? {})){
76
+ if (v !== null && v !== undefined) tags[k] = String(v);
77
+ }
78
+ if (input.surface) tags.surface = input.surface;
79
+ return tags;
80
+ }
81
+ export function buildErrorRequest(input) {
82
+ const dsn = parseDsn(input.dsn ?? "");
83
+ if (!dsn) return null;
84
+ const eventId = input.eventId ?? randomHex32();
85
+ const now = input.now ?? Date.now();
86
+ const values = describeChain(input.error).map(({ type, value, stack })=>{
87
+ const exception = {
88
+ type,
89
+ value
90
+ };
91
+ const frames = parseStack(stack);
92
+ if (frames.length) exception.stacktrace = {
93
+ frames
94
+ };
95
+ return exception;
96
+ });
97
+ const event = {
98
+ event_id: eventId,
99
+ timestamp: now / 1000,
100
+ platform: "javascript",
101
+ level: "error",
102
+ tags: cleanTags(input),
103
+ exception: {
104
+ values
105
+ }
106
+ };
107
+ if (input.release) event.release = input.release;
108
+ if (input.environment) event.environment = input.environment;
109
+ if (input.user) event.user = input.user;
110
+ if (input.extra) event.extra = input.extra;
111
+ const sentAt = new Date(now).toISOString();
112
+ const body = [
113
+ JSON.stringify({
114
+ event_id: eventId,
115
+ sent_at: sentAt,
116
+ dsn: input.dsn
117
+ }),
118
+ JSON.stringify({
119
+ type: "event"
120
+ }),
121
+ JSON.stringify(event)
122
+ ].join("\n");
123
+ return {
124
+ url: `${dsn.origin}/api/${dsn.projectId}/envelope/?sentry_key=${dsn.publicKey}&sentry_version=7`,
125
+ headers: {
126
+ "Content-Type": "application/x-sentry-envelope"
127
+ },
128
+ body
129
+ };
130
+ }
@@ -0,0 +1,145 @@
1
+ import { captureError } from "./index.js";
2
+ const LOCAL_HOSTS = new Set([
3
+ "localhost",
4
+ "127.0.0.1",
5
+ "[::1]"
6
+ ]);
7
+ export function shouldReport({ kind, error }) {
8
+ if (kind === "app") return false;
9
+ if (kind === "unknown") return true;
10
+ const status = error?.status;
11
+ return typeof status === "number" ? status >= 500 : true;
12
+ }
13
+ const STALE_CHUNK = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/;
14
+ export function isStaleChunk(error) {
15
+ const message = error?.message;
16
+ return typeof message === "string" && STALE_CHUNK.test(message);
17
+ }
18
+ const RELOAD_KEY = "heyramzi:stale-chunk-reload";
19
+ const RELOAD_WINDOW_MS = 30_000;
20
+ function browserEnv() {
21
+ try {
22
+ return {
23
+ storage: sessionStorage,
24
+ now: Date.now,
25
+ reload: ()=>location.reload()
26
+ };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ export function reloadForStaleChunk(env = browserEnv()) {
32
+ if (!env) return false;
33
+ try {
34
+ const last = Number(env.storage.getItem(RELOAD_KEY));
35
+ if (env.now() - last < RELOAD_WINDOW_MS) return false;
36
+ env.storage.setItem(RELOAD_KEY, String(env.now()));
37
+ env.reload();
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function toError(raw) {
44
+ if (raw instanceof Error) return raw;
45
+ const message = raw?.message;
46
+ return typeof message === "string" ? new Error(message) : raw;
47
+ }
48
+ const STACK_LINES = 8;
49
+ export function devErrorDetails(error) {
50
+ if (!(error instanceof Error)) {
51
+ const message = error?.message;
52
+ if (typeof message === "string") return {
53
+ message
54
+ };
55
+ try {
56
+ return {
57
+ message: typeof error === "object" ? JSON.stringify(error) : String(error)
58
+ };
59
+ } catch {
60
+ return {
61
+ message: String(error)
62
+ };
63
+ }
64
+ }
65
+ const header = String(error);
66
+ const stack = error.stack?.startsWith(header) ? error.stack.slice(header.length) : error.stack;
67
+ const frames = stack?.split("\n").map((line)=>line.trim()).filter(Boolean).slice(0, STACK_LINES);
68
+ return {
69
+ message: error.message,
70
+ stack: frames?.length ? frames.join("\n") : undefined
71
+ };
72
+ }
73
+ function isLocalHost(input) {
74
+ return LOCAL_HOSTS.has(input.event.url.hostname);
75
+ }
76
+ async function report(options, input, extraTags) {
77
+ const { dsn, dev, release, surface, environment = "production", tags } = options;
78
+ if (dev || !dsn || isLocalHost(input)) return;
79
+ await captureError({
80
+ dsn,
81
+ error: toError(input.error),
82
+ surface,
83
+ release,
84
+ environment,
85
+ tags: {
86
+ ...tags,
87
+ ...extraTags,
88
+ route: input.event.route.id
89
+ },
90
+ extra: {
91
+ pathname: input.event.url.pathname,
92
+ kind: input.kind
93
+ }
94
+ });
95
+ }
96
+ async function respondTo(options, input) {
97
+ if (!options.respond) return undefined;
98
+ try {
99
+ return await options.respond(input);
100
+ } catch (err) {
101
+ console.error("[handleError] respond threw:", err);
102
+ return undefined;
103
+ }
104
+ }
105
+ async function answer(options, input) {
106
+ const body = await respondTo(options, input);
107
+ if (!options.dev || !shouldReport(input)) return body;
108
+ return {
109
+ ...body,
110
+ ...devErrorDetails(input.error)
111
+ };
112
+ }
113
+ export function createHandleClientError(options) {
114
+ return async (input)=>{
115
+ try {
116
+ let tags;
117
+ if (isStaleChunk(input.error)) {
118
+ if (reloadForStaleChunk()) return respondTo(options, input);
119
+ tags = {
120
+ stale_chunk: true
121
+ };
122
+ }
123
+ if (shouldReport(input)) {
124
+ if (options.log !== false) console.error("[handleError]", input.error);
125
+ if (!await options.skip?.(input)) await report(options, input, tags);
126
+ }
127
+ } catch (err) {
128
+ console.error("[handleError] reporting failed:", err);
129
+ }
130
+ return answer(options, input);
131
+ };
132
+ }
133
+ export function createHandleServerError(options) {
134
+ return async (input)=>{
135
+ try {
136
+ if (shouldReport(input)) {
137
+ if (options.log !== false) console.error("[handleError]", toError(input.error));
138
+ await report(options, input);
139
+ }
140
+ } catch (err) {
141
+ console.error("[handleError] reporting failed:", err);
142
+ }
143
+ return answer(options, input);
144
+ };
145
+ }
package/dist/format.js ADDED
@@ -0,0 +1,62 @@
1
+ export function formatDate(date, locale = "en-US") {
2
+ if (!date) return "-";
3
+ const dateValue = typeof date === "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date;
4
+ const parsed = new Date(dateValue);
5
+ if (Number.isNaN(parsed.getTime())) return "-";
6
+ return parsed.toLocaleDateString(locale, {
7
+ month: "short",
8
+ day: "numeric",
9
+ year: "numeric"
10
+ });
11
+ }
12
+ export function formatCurrency(value, currency = "USD", locale) {
13
+ if (!value) return "-";
14
+ const resolvedLocale = locale ?? (currency.toUpperCase() === "EUR" ? "fr-FR" : "en-US");
15
+ return new Intl.NumberFormat(resolvedLocale, {
16
+ style: "currency",
17
+ currency: currency.toUpperCase(),
18
+ minimumFractionDigits: 0
19
+ }).format(value);
20
+ }
21
+ export function formatAmount(cents, currency, locale = "en-US") {
22
+ return new Intl.NumberFormat(locale, {
23
+ style: "currency",
24
+ currency: currency.toUpperCase(),
25
+ minimumFractionDigits: 2
26
+ }).format(cents / 100);
27
+ }
28
+ const DEFAULT_LABELS = {
29
+ today: "Today",
30
+ yesterday: "Yesterday",
31
+ daysAgo: "{n} days ago",
32
+ weeksAgo: "{n} weeks ago",
33
+ monthsAgo: "{n} months ago",
34
+ yearsAgo: "{n} years ago",
35
+ weekAgo: "1 week ago",
36
+ monthAgo: "1 month ago",
37
+ yearAgo: "1 year ago"
38
+ };
39
+ export function formatRelativeTime(date, labels = DEFAULT_LABELS) {
40
+ if (!date) return "-";
41
+ const dateValue = typeof date === "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date;
42
+ const parsed = new Date(dateValue);
43
+ if (Number.isNaN(parsed.getTime())) return "-";
44
+ const diffMs = Date.now() - parsed.getTime();
45
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
46
+ if (diffDays === 0) return labels.today;
47
+ if (diffDays === 1) return labels.yesterday;
48
+ if (diffDays < 7) return labels.daysAgo.replace("{n}", String(diffDays));
49
+ if (diffDays < 30) {
50
+ const weeks = Math.floor(diffDays / 7);
51
+ if (weeks === 1 && labels.weekAgo) return labels.weekAgo;
52
+ return labels.weeksAgo.replace("{n}", String(weeks));
53
+ }
54
+ if (diffDays < 365) {
55
+ const months = Math.floor(diffDays / 30);
56
+ if (months === 1 && labels.monthAgo) return labels.monthAgo;
57
+ return labels.monthsAgo.replace("{n}", String(months));
58
+ }
59
+ const years = Math.floor(diffDays / 365);
60
+ if (years === 1 && labels.yearAgo) return labels.yearAgo;
61
+ return labels.yearsAgo.replace("{n}", String(years));
62
+ }
package/dist/grab.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Alt+Click any element in a Svelte 5 dev build to copy its HTML and component stack for a coding agent.
3
+ */
4
+
5
+ /*
6
+ * Usage in +layout.svelte: if (dev) import("@heyramzi/utils/grab");
7
+ */
8
+
9
+ /*
10
+ * Plain JS with no imports, so a page can also inline this file as a <script type="module">.
11
+ */
12
+
13
+ const MAX_HTML = 400;
14
+
15
+ /** @param {Element} el */
16
+ function sourceOf(el) {
17
+ /** @type {Element | null} */
18
+ let node = el;
19
+ while (node && !(/** @type {any} */ (node).__svelte_meta)) node = node.parentElement;
20
+ return node ? /** @type {any} */ (node).__svelte_meta : null;
21
+ }
22
+
23
+ /** @param {{ file: string, line: number, column: number }} at */
24
+ function where(at) {
25
+ return `${at.file}:${at.line}:${at.column}`;
26
+ }
27
+
28
+ /** @param {Element} el */
29
+ function describe(el) {
30
+ // Svelte's hydration markers are <!----> comments, noise to an agent.
31
+ const full = el.outerHTML.replace(/<!--[^]*?-->/g, "");
32
+ const html = full.length > MAX_HTML ? `${full.slice(0, MAX_HTML)}…` : full;
33
+ const meta = sourceOf(el);
34
+ if (!meta) return html;
35
+
36
+ const lines = [html, ` at ${where(meta.loc)}`];
37
+ for (let entry = meta.parent; entry; entry = entry.parent) {
38
+ if (entry.type === "component") lines.push(` in <${entry.componentTag}> at ${where(entry)}`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+
43
+ /** @param {Element} el */
44
+ function label(el) {
45
+ const meta = sourceOf(el);
46
+ let entry = meta?.parent;
47
+ while (entry && entry.type !== "component") entry = entry.parent;
48
+ const name = entry ? `<${entry.componentTag}>` : el.tagName.toLowerCase();
49
+ return meta ? `${name} ${meta.loc.file.split("/").pop()}:${meta.loc.line}` : name;
50
+ }
51
+
52
+ function start() {
53
+ const box = document.createElement("div");
54
+ const tag = document.createElement("div");
55
+ box.style.cssText =
56
+ "position:fixed;pointer-events:none;z-index:2147483647;border:2px solid #7c3aed;background:rgb(124 58 237 / 0.08);border-radius:3px;display:none";
57
+ tag.style.cssText =
58
+ "position:absolute;left:-2px;bottom:100%;margin-bottom:4px;padding:2px 6px;font:12px/1.4 ui-monospace,monospace;color:#fff;background:#7c3aed;border-radius:3px;white-space:nowrap";
59
+ box.append(tag);
60
+ document.body.append(box);
61
+
62
+ /** @type {Element | null} */
63
+ let target = null;
64
+
65
+ /** @param {Element | null} el */
66
+ function show(el) {
67
+ target = el;
68
+ if (!el) {
69
+ box.style.display = "none";
70
+ return;
71
+ }
72
+ const r = el.getBoundingClientRect();
73
+ Object.assign(box.style, {
74
+ display: "block",
75
+ left: `${r.left}px`,
76
+ top: `${r.top}px`,
77
+ width: `${r.width}px`,
78
+ height: `${r.height}px`,
79
+ });
80
+ tag.textContent = label(el);
81
+ }
82
+
83
+ /** @param {MouseEvent} e */
84
+ function onMove(e) {
85
+ show(e.altKey ? document.elementFromPoint(e.clientX, e.clientY) : null);
86
+ }
87
+
88
+ /** @param {MouseEvent} e */
89
+ function onClick(e) {
90
+ if (!e.altKey || !target) return;
91
+ // Option+click on a link downloads it in Chrome, so the click is swallowed here.
92
+ e.preventDefault();
93
+ e.stopImmediatePropagation();
94
+ if (e.type !== "click") return;
95
+ navigator.clipboard.writeText(describe(target)).then(() => {
96
+ tag.textContent = "Copied";
97
+ });
98
+ }
99
+
100
+ addEventListener("mousemove", onMove, true);
101
+ addEventListener("mousedown", onClick, true);
102
+ addEventListener("click", onClick, true);
103
+ addEventListener("keyup", (e) => e.key === "Alt" && show(null), true);
104
+ addEventListener("blur", () => show(null));
105
+ }
106
+
107
+ const w = /** @type {any} */ (globalThis);
108
+ if (typeof document !== "undefined" && !w.__heyramziGrab) {
109
+ w.__heyramziGrab = true;
110
+ if (document.body) start();
111
+ else addEventListener("DOMContentLoaded", start);
112
+ }
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { cn } from "./cn.js";
2
+ export { formatDate, formatCurrency, formatAmount, formatRelativeTime } from "./format.js";
3
+ export { slugify, generateSlug, buildSearchParams, parseJSON, parseCommaSeparatedList, matchesQuery } from "./string.js";
4
+ export { fetchApi } from "./error.js";
5
+ export { isCacheExpired } from "./cache.js";
6
+ export { Ok, Err, tryCatch, tryCatchAsync } from "./result.js";
7
+ export { setLogger, log } from "./logger.js";
8
+ export { debounce, throttle, sleep } from "./timing.js";
9
+ export { clamp } from "./math.js";
10
+ export { groupBy } from "./collection.js";
File without changes
package/dist/logger.js ADDED
@@ -0,0 +1,22 @@
1
+ let _logger = null;
2
+ export function setLogger(logger) {
3
+ _logger = logger;
4
+ }
5
+ export function log(level, message, data) {
6
+ if (!_logger) return;
7
+ const entry = {
8
+ level,
9
+ message,
10
+ ...data
11
+ };
12
+ try {
13
+ const result = _logger.log(entry);
14
+ if (result instanceof Promise) {
15
+ result.catch((err)=>{
16
+ console.error("[logs] Logger failed:", err);
17
+ });
18
+ }
19
+ } catch (err) {
20
+ console.error("[logs] Logger failed:", err);
21
+ }
22
+ }
package/dist/math.js ADDED
@@ -0,0 +1,3 @@
1
+ export function clamp(value, min, max) {
2
+ return Math.min(Math.max(value, min), max);
3
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { tryCatch, tryCatchAsync } from "../result.js";
3
+ const cache = new Map();
4
+ function readRaw(path) {
5
+ const result = tryCatch(()=>readFileSync(path));
6
+ return result.ok ? result.value : null;
7
+ }
8
+ function toArrayBuffer(bytes) {
9
+ const out = new ArrayBuffer(bytes.byteLength);
10
+ new Uint8Array(out).set(bytes);
11
+ return out;
12
+ }
13
+ async function decodeFont(path) {
14
+ const cached = cache.get(path);
15
+ if (cached) return cached;
16
+ const raw = readRaw(path);
17
+ if (!raw) return null;
18
+ const isWoff2 = path.toLowerCase().endsWith(".woff2");
19
+ if (!isWoff2) {
20
+ const buf = toArrayBuffer(raw);
21
+ cache.set(path, buf);
22
+ return buf;
23
+ }
24
+ const decoded = await tryCatchAsync(async ()=>{
25
+ const { decompress } = await import("wawoff2");
26
+ return decompress(raw);
27
+ });
28
+ if (!decoded.ok) return null;
29
+ const buf = toArrayBuffer(decoded.value);
30
+ cache.set(path, buf);
31
+ return buf;
32
+ }
33
+ export async function loadFonts(configs) {
34
+ const decoded = [];
35
+ for (const config of configs){
36
+ const data = await decodeFont(config.path);
37
+ if (!data) continue;
38
+ decoded.push({
39
+ name: config.name,
40
+ data,
41
+ weight: config.weight,
42
+ style: config.style ?? "normal"
43
+ });
44
+ }
45
+ return decoded;
46
+ }
@@ -0,0 +1,3 @@
1
+ export { renderImage, MIME_TYPES } from "./renderer.js";
2
+ export { loadFonts } from "./fonts.js";
3
+ export { html, raw } from "./markup.js";
@@ -0,0 +1,31 @@
1
+ import { html as satoriHtml } from "satori-html";
2
+ import { __unsafeHTML } from "ultrahtml";
3
+ const RAW_MARKUP = Symbol.for("@heyramzi/marketing/imagegen/RawMarkup");
4
+ export function raw(markup) {
5
+ const marked = __unsafeHTML(markup);
6
+ Object.defineProperty(marked, RAW_MARKUP, {
7
+ value: true,
8
+ enumerable: false
9
+ });
10
+ return marked;
11
+ }
12
+ export function html(strings, ...values) {
13
+ const flattened = values.map((value)=>{
14
+ if (Array.isArray(value)) {
15
+ const parts = value.map((item)=>{
16
+ if (item == null || item === false) return "";
17
+ if (isRawMarkup(item)) return item.value;
18
+ return escapeHtml(String(item));
19
+ });
20
+ return raw(parts.join(""));
21
+ }
22
+ return value;
23
+ });
24
+ return satoriHtml(strings, ...flattened);
25
+ }
26
+ function isRawMarkup(value) {
27
+ return typeof value === "object" && value !== null && RAW_MARKUP in value;
28
+ }
29
+ function escapeHtml(input) {
30
+ return input.replace(/[&<>]/g, (c)=>c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
31
+ }
@@ -0,0 +1,29 @@
1
+ import satori from "satori";
2
+ import sharp from "sharp";
3
+ export async function renderImage(markup, options) {
4
+ const format = options.format ?? "png";
5
+ const svg = await satori(markup, {
6
+ width: options.size.width,
7
+ height: options.size.height,
8
+ fonts: options.fonts
9
+ });
10
+ if (format === "svg") {
11
+ return new TextEncoder().encode(svg);
12
+ }
13
+ const pipeline = sharp(Buffer.from(svg));
14
+ if (format === "avif") {
15
+ const buffer = await pipeline.avif({
16
+ quality: options.quality ?? 82
17
+ }).toBuffer();
18
+ return new Uint8Array(buffer);
19
+ }
20
+ const buffer = await pipeline.png({
21
+ quality: options.quality ?? 90
22
+ }).toBuffer();
23
+ return new Uint8Array(buffer);
24
+ }
25
+ export const MIME_TYPES = {
26
+ svg: "image/svg+xml",
27
+ png: "image/png",
28
+ avif: "image/avif"
29
+ };
File without changes
package/dist/result.js ADDED
@@ -0,0 +1,26 @@
1
+ export function Ok(value) {
2
+ return {
3
+ ok: true,
4
+ value
5
+ };
6
+ }
7
+ export function Err(error) {
8
+ return {
9
+ ok: false,
10
+ error
11
+ };
12
+ }
13
+ export function tryCatch(fn) {
14
+ try {
15
+ return Ok(fn());
16
+ } catch (e) {
17
+ return Err(e instanceof Error ? e : new Error(String(e)));
18
+ }
19
+ }
20
+ export async function tryCatchAsync(fn) {
21
+ try {
22
+ return Ok(await fn());
23
+ } catch (e) {
24
+ return Err(e instanceof Error ? e : new Error(String(e)));
25
+ }
26
+ }
package/dist/string.js ADDED
@@ -0,0 +1,32 @@
1
+ export function slugify(text) {
2
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").trim();
3
+ }
4
+ export function generateSlug(text) {
5
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
6
+ }
7
+ export function buildSearchParams(params) {
8
+ const entries = Object.entries(params).filter((entry)=>entry[1] !== undefined);
9
+ if (entries.length === 0) return "";
10
+ return `?${entries.map(([k, v])=>`${k}=${encodeURIComponent(v)}`).join("&")}`;
11
+ }
12
+ export function parseJSON(raw) {
13
+ if (!raw) return undefined;
14
+ try {
15
+ return JSON.parse(raw);
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ export function parseCommaSeparatedList(text) {
21
+ if (!text) return [];
22
+ return text.split(",").map((s)=>s.trim()).filter(Boolean);
23
+ }
24
+ function foldForSearch(text) {
25
+ return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
26
+ }
27
+ export function matchesQuery(haystack, query) {
28
+ const words = foldForSearch(query).split(/\s+/).filter(Boolean);
29
+ if (words.length === 0) return true;
30
+ const target = foldForSearch(haystack);
31
+ return words.every((word)=>target.includes(word));
32
+ }
@@ -0,0 +1,15 @@
1
+ const HTML_ESCAPES = {
2
+ "&": "&amp;",
3
+ "<": "&lt;",
4
+ ">": "&gt;",
5
+ '"': "&quot;",
6
+ "'": "&#39;"
7
+ };
8
+ function escapeHtml(input) {
9
+ return input.replace(/[&<>"']/g, (ch)=>HTML_ESCAPES[ch] ?? ch);
10
+ }
11
+ export function renderTestimonialContent(content) {
12
+ if (!content) return "";
13
+ const escaped = escapeHtml(content);
14
+ return escaped.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
15
+ }