@utilix-tech/sdk 0.2.0 → 0.3.1

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 (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +62 -25
  3. package/SECURITY.md +16 -0
  4. package/dist/{chunk-OKSWDVOM.js → chunk-GX7H6TAX.js} +1 -1
  5. package/dist/{chunk-W4UBLYFU.js → chunk-HFRTZE7T.js} +2 -2
  6. package/dist/{chunk-ROTPLW7T.js → chunk-IPR7FSX4.js} +2 -2
  7. package/dist/chunk-N7C52YGL.js +134 -0
  8. package/dist/{chunk-XST6X3HT.js → chunk-QJE743LY.js} +10 -10
  9. package/dist/{chunk-NSOARQCM.js → chunk-V5S6OJ4A.js} +14 -14
  10. package/dist/{chunk-XXYZLLHI.js → chunk-YBBYFAOV.js} +167 -1
  11. package/dist/index.cjs +327 -29
  12. package/dist/index.d.cts +2 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +7 -6
  15. package/dist/media-DF2cx3pK.d.cts +28 -0
  16. package/dist/media-DF2cx3pK.d.ts +28 -0
  17. package/dist/{text-DqAjPtQ0.d.cts → text-CI7JAl7F.d.cts} +28 -2
  18. package/dist/{text-DqAjPtQ0.d.ts → text-CI7JAl7F.d.ts} +28 -2
  19. package/dist/tools/api.cjs +2 -2
  20. package/dist/tools/api.js +1 -1
  21. package/dist/tools/code.cjs +14 -14
  22. package/dist/tools/code.js +1 -1
  23. package/dist/tools/encoding.cjs +2 -2
  24. package/dist/tools/encoding.js +1 -1
  25. package/dist/tools/media.cjs +130 -0
  26. package/dist/tools/media.d.cts +1 -0
  27. package/dist/tools/media.d.ts +1 -0
  28. package/dist/tools/media.js +2 -0
  29. package/dist/tools/misc.cjs +1 -1
  30. package/dist/tools/misc.js +1 -1
  31. package/dist/tools/text.cjs +166 -0
  32. package/dist/tools/text.d.cts +1 -1
  33. package/dist/tools/text.d.ts +1 -1
  34. package/dist/tools/text.js +1 -1
  35. package/dist/tools/units.cjs +10 -10
  36. package/dist/tools/units.js +1 -1
  37. package/package.json +23 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
+
5
+ ## 0.3.1
6
+ - Metadata fixes: corrected repository/bug tracker links, updated tool count in the package description.
7
+
8
+ ## 0.3.0
9
+ - Added the `media` module.
10
+
11
+ ## 0.2.0
12
+ - 400+ functions across 14 modules, including the `ai_agent` module for LLM/RAG pipelines.
13
+
14
+ ## 0.1.0
15
+ - Initial release.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **100+ developer utility functions for Node.js runs locally, no API key required.**
3
+ **140+ developer utility functions for Node.js: runs locally, no API key required.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@utilix-tech/sdk?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@utilix-tech/sdk)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@utilix-tech/sdk?style=flat-square)](https://www.npmjs.com/package/@utilix-tech/sdk)
@@ -26,13 +26,13 @@ yarn add @utilix-tech/sdk
26
26
  pnpm add @utilix-tech/sdk
27
27
  ```
28
28
 
29
- **Requires Node.js 18 or later.** TypeScript types are bundled no `@types/` package needed.
29
+ **Requires Node.js 18 or later.** The public npm package includes bundled TypeScript types, so no `@types/` package is needed.
30
30
 
31
31
  ---
32
32
 
33
33
  ## Quick Start
34
34
 
35
- Each of the 14 modules is available as a dedicated subpath import. Import only what you use; bundlers automatically tree-shake the rest.
35
+ Each of the 16 modules is available as a dedicated subpath import. Import only what you use; bundlers automatically tree-shake the rest.
36
36
 
37
37
  ```ts
38
38
  // Pick exactly the modules you need
@@ -62,7 +62,7 @@ const { formatJson } = require("@utilix-tech/sdk/json");
62
62
 
63
63
  ## Modules
64
64
 
65
- ### `/json` JSON Tools
65
+ ### `/json`: JSON Tools
66
66
 
67
67
  ```ts
68
68
  import { formatJson, minifyJson, diffJson, jsonToTypescript, validateJson,
@@ -71,7 +71,7 @@ import { formatJson, minifyJson, diffJson, jsonToTypescript, validateJson,
71
71
  // Format with 2-space indent
72
72
  formatJson('{"name":"alice","age":30}', { indent: 2 });
73
73
 
74
- // Diff two JSON strings returns line-by-line diff
74
+ // Diff two JSON strings: returns line-by-line diff
75
75
  diffJson(jsonA, jsonB);
76
76
 
77
77
  // Generate TypeScript interface from JSON
@@ -89,7 +89,7 @@ validateJson(data, schema);
89
89
 
90
90
  ---
91
91
 
92
- ### `/encoding` Encode & Decode
92
+ ### `/encoding`: Encode & Decode
93
93
 
94
94
  ```ts
95
95
  import { encodeBase64, decodeBase64, encodeUrl, decodeUrl,
@@ -104,7 +104,7 @@ base32Encode("hello"); // "NBSWY3DPEB3W64TMMQ======"
104
104
 
105
105
  ---
106
106
 
107
- ### `/hashing` Hash & Password
107
+ ### `/hashing`: Hash & Password
108
108
 
109
109
  ```ts
110
110
  import { hashAll, hashOne, hashPassword, verifyPassword, generateHtpasswdFile } from "@utilix-tech/sdk/hashing";
@@ -123,11 +123,11 @@ generateHtpasswdFile([{ username: "alice", password: "secret" }]);
123
123
 
124
124
  ---
125
125
 
126
- ### `/text` String & Text
126
+ ### `/text`: String & Text
127
127
 
128
128
  ```ts
129
129
  import { convertCase, slugify, countWords, generateWords, generateParagraphs,
130
- escapeString, htmlToMarkdown, applyOps } from "@utilix-tech/sdk/text";
130
+ escapeString, htmlToMarkdown, applyOps, detectPassiveVoice } from "@utilix-tech/sdk/text";
131
131
 
132
132
  convertCase("hello world", "camelCase"); // "helloWorld"
133
133
  convertCase("hello world", "PascalCase"); // "HelloWorld"
@@ -145,11 +145,14 @@ generateParagraphs(3); // lorem ipsum paragraphs
145
145
  applyOps(text, ["sort", "deduplicate", "trim"]);
146
146
 
147
147
  htmlToMarkdown("<h1>Hello</h1><p>World</p>");
148
+
149
+ // Flag passive-voice sentences, e.g. "was written", "were approved by the team"
150
+ detectPassiveVoice("The report was reviewed by the committee. They approved it.");
148
151
  ```
149
152
 
150
153
  ---
151
154
 
152
- ### `/data` CSV / YAML / TOML / XML / INI
155
+ ### `/data`: CSV / YAML / TOML / XML / INI
153
156
 
154
157
  ```ts
155
158
  import { parseCsv, csvToJson, validateYaml, tomlToJson, jsonToToml,
@@ -176,7 +179,7 @@ parseIni("[db]\nhost=localhost\nport=5432");
176
179
 
177
180
  ---
178
181
 
179
- ### `/generators` UUID, Passwords, Fake Data
182
+ ### `/generators`: UUID, Passwords, Fake Data
180
183
 
181
184
  ```ts
182
185
  import { generateUuid, generateV4, generateV7, generateUlid,
@@ -199,7 +202,7 @@ generateData([
199
202
 
200
203
  ---
201
204
 
202
- ### `/time` Dates, Timezones, Cron
205
+ ### `/time`: Dates, Timezones, Cron
203
206
 
204
207
  ```ts
205
208
  import { diffDates, parseCron, convertTime, formatRelative,
@@ -222,7 +225,7 @@ getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
222
225
 
223
226
  ---
224
227
 
225
- ### `/units` Conversions
228
+ ### `/units`: Conversions
226
229
 
227
230
  ```ts
228
231
  import { convertBytes, pxToAll, convert, formatValue } from "@utilix-tech/sdk/units";
@@ -247,7 +250,7 @@ formatValue(1234567.89, { locale: "en-US", currency: "USD" });
247
250
 
248
251
  ---
249
252
 
250
- ### `/network` URLs, IPs, HTTP
253
+ ### `/network`: URLs, IPs, HTTP
251
254
 
252
255
  ```ts
253
256
  import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
@@ -267,7 +270,7 @@ countryCodeToFlag("US"); // "🇺🇸"
267
270
 
268
271
  ---
269
272
 
270
- ### `/api` cURL, JWT, CORS, HTTP
273
+ ### `/api`: cURL, JWT, CORS, HTTP
271
274
 
272
275
  ```ts
273
276
  import { buildCurl, parseCurlCommand, decodeJwt, signJwt,
@@ -286,7 +289,7 @@ buildCurl({
286
289
  // Parse a curl command back into its parts
287
290
  parseCurlCommand('curl -X GET https://api.example.com -H "Authorization: Bearer token"');
288
291
 
289
- // JWT decode without verification
292
+ // JWT: decode without verification
290
293
  decodeJwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");
291
294
  // { header: {...}, payload: {...}, isExpired: false, expiresIn: "2h" }
292
295
 
@@ -299,7 +302,7 @@ generateCspHeader({ "default-src": ["'self'"], "script-src": ["'self'", "cdn.exa
299
302
 
300
303
  ---
301
304
 
302
- ### `/code` SQL, HTML, RegEx, GQL
305
+ ### `/code`: SQL, HTML, RegEx, GQL
303
306
 
304
307
  ```ts
305
308
  import { formatSql, minifySql, testRegex, formatHtml, formatGql,
@@ -331,7 +334,7 @@ formatGql("query { user(id: 1) { name email } }");
331
334
 
332
335
  ---
333
336
 
334
- ### `/color` Color Conversion & Palettes
337
+ ### `/color`: Color Conversion & Palettes
335
338
 
336
339
  ```ts
337
340
  import { parseColor, convertColor, generatePalette, checkContrast } from "@utilix-tech/sdk/color";
@@ -351,7 +354,7 @@ checkContrast("#ffffff", "#000000");
351
354
 
352
355
  ---
353
356
 
354
- ### `/css` CSS Generators
357
+ ### `/css`: CSS Generators
355
358
 
356
359
  ```ts
357
360
  import { generateGradient, generateBoxShadow, generateBorderRadius,
@@ -373,7 +376,7 @@ generateBorderRadius({ tl: 8, tr: 8, br: 0, bl: 0 });
373
376
 
374
377
  ---
375
378
 
376
- ### `/misc` SVG, QR, Unicode
379
+ ### `/misc`: SVG, QR, Unicode
377
380
 
378
381
  ```ts
379
382
  import { optimizeSvg, sanitizeSvg, analyzeString, toUnicodeEscape,
@@ -397,6 +400,22 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
397
400
 
398
401
  ---
399
402
 
403
+ ### `/media`: Image Header Parsing
404
+
405
+ ```ts
406
+ import { readImageInfo } from "@utilix-tech/sdk/media";
407
+
408
+ // Reads format, dimensions, bit depth, and alpha channel straight from
409
+ // file bytes: no decoding, no canvas, no image library.
410
+ const bytes = await fs.promises.readFile("photo.png");
411
+ readImageInfo(new Uint8Array(bytes));
412
+ // { format: "png", width: 1920, height: 1080, bitDepth: 8, colorType: "rgba", hasAlpha: true }
413
+ ```
414
+
415
+ Supports PNG, JPEG, GIF, WebP, and BMP.
416
+
417
+ ---
418
+
400
419
  ## All Modules at a Glance
401
420
 
402
421
  | Import | What it does |
@@ -415,12 +434,14 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
415
434
  | `@utilix-tech/sdk/color` | Hex/RGB/HSL/HSV/CMYK convert, palette generation, contrast check |
416
435
  | `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
417
436
  | `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
437
+ | `@utilix-tech/sdk/ai_agent` | Token estimate/trim, chunk text, extract URLs/JSON/keywords, sanitize HTML, flatten/merge JSON, dedupe lines, validate schema, PII/secret/injection detect |
438
+ | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes: PNG, JPEG, GIF, WebP, BMP |
418
439
 
419
440
  ---
420
441
 
421
442
  ## Python SDK
422
443
 
423
- `@utilix-tech/sdk` has a Python companion [`utilix-sdk`](https://pypi.org/project/utilix-sdk/) on PyPI with the same 100+ tools and identical return shapes.
444
+ `@utilix-tech/sdk` has a Python companion, [`utilix-sdk`](https://pypi.org/project/utilix-sdk/) on PyPI, with the same 140+ tools and identical return shapes.
424
445
 
425
446
  ```bash
426
447
  pip install utilix-sdk
@@ -430,14 +451,30 @@ Both SDKs return plain JSON-serializable objects so you can share test fixtures
430
451
 
431
452
  ---
432
453
 
433
- ## Surface A vs Surface B
454
+ ## REST API
455
+
456
+ **This package is Surface A**: everything runs in-process in your Node.js runtime. No outbound requests, no API key, no rate limits.
434
457
 
435
- **This package is Surface A** everything runs in-process in your Node.js runtime. No outbound requests, no API key, no rate limits.
458
+ The same 140+ tools are also available as a hosted **REST API** at `https://api.utilix.tech/v1` for environments where you can't ship a Node.js runtime, or for cross-language teams.
436
459
 
437
- A **Surface B REST API** (`api.utilix.tech`) is coming soon for use cases where you can't ship a Node.js or Python runtime. Same tools, callable from any language over HTTP.
460
+ ```bash
461
+ curl -X POST https://api.utilix.tech/v1/tools/hash \
462
+ -H "Authorization: Bearer utx_live_..." \
463
+ -H "Content-Type: application/json" \
464
+ -d '{"input": "hello world", "algorithm": "sha256"}'
465
+ ```
466
+
467
+ - **Free**: 1,000 requests/day: no credit card required
468
+ - **Pro**: 10,000 requests/day: $9/month
469
+ - Try it live at **[utilix.tech/api](https://utilix.tech/api)**: no signup needed for the first 10 endpoints
470
+ - Get your API key at **[utilix.tech/dashboard](https://utilix.tech/dashboard)**
438
471
 
439
472
  ---
440
473
 
474
+ ## Publishing (maintainers)
475
+
476
+ CI publishes automatically via npm Trusted Publishing (OIDC) on every push to `main` that bumps `version`. Requires npm **>=11.5.1**: older versions (e.g. the npm bundled with Node 20) sign provenance but skip the OIDC auth exchange, so `npm publish` fails with a 404 as if unauthenticated.
477
+
441
478
  ## License
442
479
 
443
- MIT see [LICENSE](./LICENSE) for details.
480
+ MIT: see [LICENSE](./LICENSE) for details.
package/SECURITY.md ADDED
@@ -0,0 +1,16 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability in this package, please report it privately rather than opening a public issue.
6
+
7
+ Email **hello@utilix.tech** with the subject line "Security report" and include:
8
+ - A description of the vulnerability and its potential impact
9
+ - Steps to reproduce, if possible
10
+ - Any suggested fix
11
+
12
+ We aim to acknowledge reports within 3 business days. Please give us a reasonable window to release a fix before disclosing publicly.
13
+
14
+ ## Supported Versions
15
+
16
+ Only the latest published version of `@utilix-tech/sdk` receives security fixes. We recommend always running the latest release.
@@ -570,7 +570,7 @@ function getConversionWarnings(from, to) {
570
570
  warnings.push("Transparency will be lost if the WebP image has an alpha channel");
571
571
  }
572
572
  if (to === "image/jpeg") {
573
- warnings.push("JPEG uses lossy compression \u2014 some quality may be reduced");
573
+ warnings.push("JPEG uses lossy compression: some quality may be reduced");
574
574
  }
575
575
  return warnings;
576
576
  }
@@ -451,13 +451,13 @@ function decodeJwt(token) {
451
451
  try {
452
452
  header = decodeSegment(rawHeader);
453
453
  } catch {
454
- return { ok: false, error: "Failed to decode header \u2014 not valid base64url JSON" };
454
+ return { ok: false, error: "Failed to decode header: not valid base64url JSON" };
455
455
  }
456
456
  let payload;
457
457
  try {
458
458
  payload = decodeSegment(rawPayload);
459
459
  } catch {
460
- return { ok: false, error: "Failed to decode payload \u2014 not valid base64url JSON" };
460
+ return { ok: false, error: "Failed to decode payload: not valid base64url JSON" };
461
461
  }
462
462
  const algorithm = typeof header.alg === "string" ? header.alg : "unknown";
463
463
  const now = Math.floor(Date.now() / 1e3);
@@ -45,7 +45,7 @@ function decodeBase64(input, variant = "standard") {
45
45
  const output = bytes.toString("utf8");
46
46
  return { ok: true, output, outputBytes: bytes.length };
47
47
  } catch {
48
- return { ok: false, error: "Invalid Base64 \u2014 check the input and variant setting" };
48
+ return { ok: false, error: "Invalid Base64: check the input and variant setting" };
49
49
  }
50
50
  }
51
51
  function encodeUrl(input, mode = "component") {
@@ -64,7 +64,7 @@ function decodeUrl(input, mode = "component") {
64
64
  const output = mode === "component" ? decodeURIComponent(trimmed) : decodeURI(trimmed);
65
65
  return { ok: true, output, changed: output !== trimmed };
66
66
  } catch {
67
- return { ok: false, error: "Invalid percent-encoding \u2014 check the input" };
67
+ return { ok: false, error: "Invalid percent-encoding: check the input" };
68
68
  }
69
69
  }
70
70
  function diffSegments(original, encoded) {
@@ -0,0 +1,134 @@
1
+ import { __export } from './chunk-MLKGABMK.js';
2
+
3
+ // src/tools/media.ts
4
+ var media_exports = {};
5
+ __export(media_exports, {
6
+ readImageInfo: () => readImageInfo
7
+ });
8
+ function readU32BE(bytes, offset) {
9
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
10
+ }
11
+ function readI32LE(bytes, offset) {
12
+ return bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24;
13
+ }
14
+ var PNG_COLOR_TYPES = {
15
+ 0: "grayscale",
16
+ 2: "rgb",
17
+ 3: "palette",
18
+ 4: "grayscale+alpha",
19
+ 6: "rgba"
20
+ };
21
+ function readPng(bytes) {
22
+ if (bytes.length < 33) return { error: "Truncated PNG file" };
23
+ const width = readU32BE(bytes, 16);
24
+ const height = readU32BE(bytes, 20);
25
+ const bitDepth = bytes[24];
26
+ const colorTypeNum = bytes[25];
27
+ const colorType = PNG_COLOR_TYPES[colorTypeNum] ?? "unknown";
28
+ const hasAlpha = colorTypeNum === 4 || colorTypeNum === 6;
29
+ return { format: "png", width, height, bitDepth, colorType, hasAlpha };
30
+ }
31
+ function readGif(bytes) {
32
+ if (bytes.length < 10) return { error: "Truncated GIF file" };
33
+ const width = bytes[6] | bytes[7] << 8;
34
+ const height = bytes[8] | bytes[9] << 8;
35
+ return { format: "gif", width, height };
36
+ }
37
+ function readBmp(bytes) {
38
+ if (bytes.length < 30) return { error: "Truncated BMP file" };
39
+ const width = readI32LE(bytes, 18);
40
+ const heightRaw = readI32LE(bytes, 22);
41
+ const bitDepth = bytes[28] | bytes[29] << 8;
42
+ return { format: "bmp", width: Math.abs(width), height: Math.abs(heightRaw), bitDepth };
43
+ }
44
+ function readWebp(bytes) {
45
+ if (bytes.length < 16) return { error: "Truncated WebP file" };
46
+ const fourCC = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
47
+ if (fourCC === "VP8 ") {
48
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
49
+ const width = (bytes[26] | bytes[27] << 8) & 16383;
50
+ const height = (bytes[28] | bytes[29] << 8) & 16383;
51
+ return { format: "webp", width, height };
52
+ }
53
+ if (fourCC === "VP8L") {
54
+ if (bytes.length < 25) return { error: "Truncated WebP file" };
55
+ const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
56
+ const width = (bits & 16383) + 1;
57
+ const height = (bits >>> 14 & 16383) + 1;
58
+ const hasAlpha = !!(bits >>> 28 & 1);
59
+ return { format: "webp", width, height, hasAlpha };
60
+ }
61
+ if (fourCC === "VP8X") {
62
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
63
+ const flags = bytes[20];
64
+ const hasAlpha = !!(flags & 16);
65
+ const width = (bytes[24] | bytes[25] << 8 | bytes[26] << 16) + 1;
66
+ const height = (bytes[27] | bytes[28] << 8 | bytes[29] << 16) + 1;
67
+ return { format: "webp", width, height, hasAlpha };
68
+ }
69
+ return { error: "Unrecognized WebP chunk format" };
70
+ }
71
+ var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
72
+ 192,
73
+ 193,
74
+ 194,
75
+ 195,
76
+ 197,
77
+ 198,
78
+ 199,
79
+ 201,
80
+ 202,
81
+ 203,
82
+ 205,
83
+ 206,
84
+ 207
85
+ ]);
86
+ function readJpeg(bytes) {
87
+ let offset = 2;
88
+ while (offset < bytes.length - 1) {
89
+ if (bytes[offset] !== 255) {
90
+ offset++;
91
+ continue;
92
+ }
93
+ const marker = bytes[offset + 1];
94
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
95
+ offset += 2;
96
+ continue;
97
+ }
98
+ if (marker === 217) break;
99
+ if (offset + 3 >= bytes.length) break;
100
+ const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
101
+ if (JPEG_SOF_MARKERS.has(marker)) {
102
+ if (offset + 9 >= bytes.length) return { error: "Truncated JPEG SOF segment" };
103
+ const bitDepth = bytes[offset + 4];
104
+ const height = bytes[offset + 5] << 8 | bytes[offset + 6];
105
+ const width = bytes[offset + 7] << 8 | bytes[offset + 8];
106
+ const components = bytes[offset + 9];
107
+ const colorType = components === 1 ? "grayscale" : components === 3 ? "rgb" : components === 4 ? "cmyk" : "unknown";
108
+ return { format: "jpeg", width, height, bitDepth, colorType };
109
+ }
110
+ offset += 2 + segmentLength;
111
+ }
112
+ return { error: "No SOF marker found in JPEG (file may be corrupt)" };
113
+ }
114
+ function readImageInfo(bytes) {
115
+ if (bytes.length < 10) return { error: "File too small to determine format" };
116
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
117
+ return readPng(bytes);
118
+ }
119
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
120
+ return readGif(bytes);
121
+ }
122
+ if (bytes[0] === 66 && bytes[1] === 77) {
123
+ return readBmp(bytes);
124
+ }
125
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
126
+ return readWebp(bytes);
127
+ }
128
+ if (bytes[0] === 255 && bytes[1] === 216) {
129
+ return readJpeg(bytes);
130
+ }
131
+ return { error: "Unrecognized image format. Supported formats: PNG, JPEG, GIF, WebP, BMP" };
132
+ }
133
+
134
+ export { media_exports, readImageInfo };
@@ -82,16 +82,16 @@ function formatValue(value, unit, decimals = 4) {
82
82
  return `${value.toFixed(decimals)}${unit}`;
83
83
  }
84
84
  var COMMON_SIZES = [
85
- { px: 8, label: "8px \u2014 Extra small spacing / icon size" },
86
- { px: 12, label: "12px \u2014 Small text / caption" },
87
- { px: 14, label: "14px \u2014 Body small / secondary text" },
88
- { px: 16, label: "16px \u2014 Base font size / body text" },
89
- { px: 18, label: "18px \u2014 Body large / lead text" },
90
- { px: 20, label: "20px \u2014 Subheading / large body" },
91
- { px: 24, label: "24px \u2014 H4 / section title" },
92
- { px: 32, label: "32px \u2014 H3 / card heading" },
93
- { px: 48, label: "48px \u2014 H2 / page section heading" },
94
- { px: 64, label: "64px \u2014 H1 / hero heading" }
85
+ { px: 8, label: "8px: Extra small spacing / icon size" },
86
+ { px: 12, label: "12px: Small text / caption" },
87
+ { px: 14, label: "14px: Body small / secondary text" },
88
+ { px: 16, label: "16px: Base font size / body text" },
89
+ { px: 18, label: "18px: Body large / lead text" },
90
+ { px: 20, label: "20px: Subheading / large body" },
91
+ { px: 24, label: "24px: H4 / section title" },
92
+ { px: 32, label: "32px: H3 / card heading" },
93
+ { px: 48, label: "48px: H2 / page section heading" },
94
+ { px: 64, label: "64px: H1 / hero heading" }
95
95
  ];
96
96
  function pxToViewport(px, viewportWidth = 1440, viewportHeight = 900) {
97
97
  const vw = parseFloat((px / viewportWidth * 100).toFixed(4));
@@ -134,24 +134,24 @@ var CHEATSHEET = [
134
134
  { pattern: "{n}", description: "Matches exactly n repetitions", example: "\\d{3} matches exactly 3 digits", category: "Quantifiers" },
135
135
  { pattern: "{n,}", description: "Matches n or more repetitions", example: "\\d{2,} matches 2 or more digits", category: "Quantifiers" },
136
136
  { pattern: "{n,m}", description: "Matches between n and m repetitions", example: "\\d{2,4} matches 2 to 4 digits", category: "Quantifiers" },
137
- { pattern: "*?", description: "Lazy match \u2014 matches as few characters as possible", example: "<.*?> matches the shortest possible HTML tag", category: "Quantifiers" },
137
+ { pattern: "*?", description: "Lazy match: matches as few characters as possible", example: "<.*?> matches the shortest possible HTML tag", category: "Quantifiers" },
138
138
  { pattern: "+?", description: "Lazy version of +", example: "a+? matches as few 'a's as possible", category: "Quantifiers" },
139
- { pattern: "(abc)", description: "Capturing group \u2014 captures the matched substring", example: "(\\d+) captures digit sequences", category: "Groups" },
140
- { pattern: "(?:abc)", description: "Non-capturing group \u2014 groups without capturing", example: "(?:foo|bar)baz matches 'foobaz' or 'barbaz'", category: "Groups" },
139
+ { pattern: "(abc)", description: "Capturing group: captures the matched substring", example: "(\\d+) captures digit sequences", category: "Groups" },
140
+ { pattern: "(?:abc)", description: "Non-capturing group: groups without capturing", example: "(?:foo|bar)baz matches 'foobaz' or 'barbaz'", category: "Groups" },
141
141
  { pattern: "(?<name>abc)", description: "Named capturing group", example: "(?<year>\\d{4}) captures year by name", category: "Groups" },
142
- { pattern: "a|b", description: "Alternation \u2014 matches either a or b", example: "cat|dog matches 'cat' or 'dog'", category: "Groups" },
142
+ { pattern: "a|b", description: "Alternation: matches either a or b", example: "cat|dog matches 'cat' or 'dog'", category: "Groups" },
143
143
  { pattern: "\\1", description: "Backreference to the first capturing group", example: "(\\w+) \\1 matches repeated words like 'the the'", category: "Groups" },
144
144
  { pattern: "\\k<name>", description: "Backreference to a named capturing group", example: "(?<word>\\w+) \\k<word> matches repeated named group", category: "Groups" },
145
- { pattern: "(?=abc)", description: "Positive lookahead \u2014 asserts what follows matches", example: "\\d+(?= dollars) matches digits followed by ' dollars'", category: "Lookahead/Lookbehind" },
146
- { pattern: "(?!abc)", description: "Negative lookahead \u2014 asserts what follows does not match", example: "\\d+(?! dollars) matches digits not followed by ' dollars'", category: "Lookahead/Lookbehind" },
147
- { pattern: "(?<=abc)", description: "Positive lookbehind \u2014 asserts what precedes matches", example: "(?<=\\$)\\d+ matches digits preceded by '$'", category: "Lookahead/Lookbehind" },
148
- { pattern: "(?<!abc)", description: "Negative lookbehind \u2014 asserts what precedes does not match", example: "(?<!\\$)\\d+ matches digits not preceded by '$'", category: "Lookahead/Lookbehind" },
149
- { pattern: "g", description: "Global flag \u2014 finds all matches rather than stopping at the first", example: "/\\d+/g finds all numbers in a string", category: "Flags" },
150
- { pattern: "i", description: "Case-insensitive flag \u2014 ignores letter case", example: "/hello/i matches 'Hello', 'HELLO', 'hello'", category: "Flags" },
151
- { pattern: "m", description: "Multiline flag \u2014 ^ and $ match start/end of each line", example: "/^\\w+/m matches the first word of each line", category: "Flags" },
152
- { pattern: "s", description: "Dotall flag \u2014 makes . match newline characters too", example: "/a.b/s matches 'a\\nb'", category: "Flags" },
153
- { pattern: "u", description: "Unicode flag \u2014 enables full Unicode matching", example: "/\\u{1F600}/u matches a Unicode emoji", category: "Flags" },
154
- { pattern: "y", description: "Sticky flag \u2014 matches only from the lastIndex position", example: "/\\d+/y matches digits starting at the current position", category: "Flags" },
145
+ { pattern: "(?=abc)", description: "Positive lookahead: asserts what follows matches", example: "\\d+(?= dollars) matches digits followed by ' dollars'", category: "Lookahead/Lookbehind" },
146
+ { pattern: "(?!abc)", description: "Negative lookahead: asserts what follows does not match", example: "\\d+(?! dollars) matches digits not followed by ' dollars'", category: "Lookahead/Lookbehind" },
147
+ { pattern: "(?<=abc)", description: "Positive lookbehind: asserts what precedes matches", example: "(?<=\\$)\\d+ matches digits preceded by '$'", category: "Lookahead/Lookbehind" },
148
+ { pattern: "(?<!abc)", description: "Negative lookbehind: asserts what precedes does not match", example: "(?<!\\$)\\d+ matches digits not preceded by '$'", category: "Lookahead/Lookbehind" },
149
+ { pattern: "g", description: "Global flag: finds all matches rather than stopping at the first", example: "/\\d+/g finds all numbers in a string", category: "Flags" },
150
+ { pattern: "i", description: "Case-insensitive flag: ignores letter case", example: "/hello/i matches 'Hello', 'HELLO', 'hello'", category: "Flags" },
151
+ { pattern: "m", description: "Multiline flag: ^ and $ match start/end of each line", example: "/^\\w+/m matches the first word of each line", category: "Flags" },
152
+ { pattern: "s", description: "Dotall flag: makes . match newline characters too", example: "/a.b/s matches 'a\\nb'", category: "Flags" },
153
+ { pattern: "u", description: "Unicode flag: enables full Unicode matching", example: "/\\u{1F600}/u matches a Unicode emoji", category: "Flags" },
154
+ { pattern: "y", description: "Sticky flag: matches only from the lastIndex position", example: "/\\d+/y matches digits starting at the current position", category: "Flags" },
155
155
  { pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", description: "Matches a basic email address", example: "user@example.com", category: "Common Patterns" },
156
156
  { pattern: "https?://[\\w\\-]+(\\.[\\w\\-]+)+([\\w.,@?^=%&:/~+#\\-]*[\\w@?^=%&/~+#\\-])?", description: "Matches an HTTP or HTTPS URL", example: "https://www.example.com/path?query=value", category: "Common Patterns" },
157
157
  { pattern: "^(?:\\+1)?[-.\\s]?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$", description: "Matches a North American phone number in various formats", example: "(555) 123-4567 or 555-123-4567", category: "Common Patterns" },