@utilix-tech/sdk 0.45.0 → 0.47.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 (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +36 -9
  3. package/dist/{chunk-L6V4W4SU.js → chunk-226EH3ZI.js} +87 -1
  4. package/dist/{chunk-DFLHX4NW.js → chunk-KMQHBUYO.js} +55 -1
  5. package/dist/{chunk-F2CEN226.js → chunk-P7TXG7CI.js} +127 -1
  6. package/dist/{chunk-7PEKYLRB.js → chunk-S3OA5VCX.js} +50 -1
  7. package/dist/index.cjs +315 -0
  8. package/dist/index.d.cts +4 -4
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +4 -4
  11. package/dist/{media-fD8dJ5qR.d.cts → media-BUvwm4wU.d.cts} +27 -2
  12. package/dist/{media-fD8dJ5qR.d.ts → media-BUvwm4wU.d.ts} +27 -2
  13. package/dist/{network-Bzm1tjMA.d.cts → network-BG0XDx5W.d.cts} +40 -2
  14. package/dist/{network-Bzm1tjMA.d.ts → network-BG0XDx5W.d.ts} +40 -2
  15. package/dist/{text-kHkT9VtT.d.cts → text-DWUcY8H1.d.cts} +34 -2
  16. package/dist/{text-kHkT9VtT.d.ts → text-DWUcY8H1.d.ts} +34 -2
  17. package/dist/tools/media.cjs +86 -0
  18. package/dist/tools/media.d.cts +1 -1
  19. package/dist/tools/media.d.ts +1 -1
  20. package/dist/tools/media.js +1 -1
  21. package/dist/tools/network.cjs +49 -0
  22. package/dist/tools/network.d.cts +1 -1
  23. package/dist/tools/network.d.ts +1 -1
  24. package/dist/tools/network.js +1 -1
  25. package/dist/tools/text.cjs +126 -0
  26. package/dist/tools/text.d.cts +1 -1
  27. package/dist/tools/text.d.ts +1 -1
  28. package/dist/tools/text.js +1 -1
  29. package/dist/tools/units.cjs +54 -0
  30. package/dist/tools/units.d.cts +1 -1
  31. package/dist/tools/units.d.ts +1 -1
  32. package/dist/tools/units.js +1 -1
  33. package/dist/{units-DWRmbS_a.d.cts → units-DU4Pisfx.d.cts} +36 -2
  34. package/dist/{units-DWRmbS_a.d.ts → units-DU4Pisfx.d.ts} +36 -2
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
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
4
 
5
+ ## 0.47.0
6
+ - Add `buildContentDisposition`, `parseContentDisposition` (content-disposition-builder): build a `Content-Disposition` response header from a disposition type and filename, with an RFC 5987 `filename*` extended parameter for non-ASCII filenames, or parse an existing header into its type, filename, and raw parameters.
7
+ - Add `inspectGifFrames` (gif-frame-inspector): walk a GIF87a/89a file's block structure to report frame count, total animation duration, and loop count, without decoding any LZW-compressed pixel data.
8
+
9
+ ## 0.46.0
10
+ - Add `calculateDepreciationSchedule` (depreciation-schedule-calculator): build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits.
11
+ - Add `analyzeKeywordDensity` (keyword-density-analyzer): count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes.
12
+
5
13
  ## 0.45.0
6
14
  - Add `calculateCdMaturity` (cd-maturity-calculator): compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net payout if withdrawn early.
7
15
  - Add `buildClientHints`, `parseClientHintHeaders` (client-hints-header-builder): build an `Accept-CH`/`Critical-CH` response header from a list of client hint names with a plain-English note per hint, or decode raw Client Hints request-header text into structured values.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **630 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **632 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)
@@ -149,7 +149,7 @@ generateEtag("console.log(1)");
149
149
  import { convertCase, slugify, countWords, generateWords, generateParagraphs,
150
150
  escapeString, htmlToMarkdown, applyOps, detectPassiveVoice, scoreReadability,
151
151
  detectFillerWords, calculateStringDistance, analyzeSentenceLengths,
152
- searchEmoji, getEmojiByShortcode } from "@utilix-tech/sdk/text";
152
+ analyzeKeywordDensity, searchEmoji, getEmojiByShortcode } from "@utilix-tech/sdk/text";
153
153
 
154
154
  convertCase("hello world", "camelCase"); // "helloWorld"
155
155
  convertCase("hello world", "PascalCase"); // "HelloWorld"
@@ -190,6 +190,10 @@ analyzeSentenceLengths("The cat sat on the mat. It was a sunny day, and the dog
190
190
  // Search ~280 common emoji by name, keyword, or GitHub-style shortcode
191
191
  searchEmoji("fire"); // [{ emoji: "🔥", name: "fire", shortcode: "fire", codepoint: "U+1F525", ... }]
192
192
  getEmojiByShortcode(":joy:"); // { emoji: "😂", name: "face with tears of joy", ... }
193
+
194
+ // Keyword/phrase frequency and density (occurrences per 100 words); ngramSize 2 or 3 for phrases
195
+ analyzeKeywordDensity("machine learning models power machine learning systems");
196
+ // { totalWords: 7, uniqueKeywords: 5, ngramSize: 1, keywords: [{ keyword: "learning", count: 2, density: 28.6 }, ...] }
193
197
  ```
194
198
 
195
199
  ---
@@ -312,7 +316,7 @@ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
312
316
  ### `/units`: Conversions
313
317
 
314
318
  ```ts
315
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity } from "@utilix-tech/sdk/units";
319
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity, calculateDepreciationSchedule } from "@utilix-tech/sdk/units";
316
320
 
317
321
  // File sizes
318
322
  convertBytes(1073741824, "GB"); // 1
@@ -430,6 +434,10 @@ calculatePaybackPeriod({ initialInvestment: 1000, cashFlows: [400, 400, 400], di
430
434
  // Certificate of deposit maturity value, APY, and net early-withdrawal payout
431
435
  calculateCdMaturity(1000, 12, 12, 12, 3);
432
436
  // { maturityValue: 1126.83, totalInterest: 126.83, apy: 12.6825, earlyWithdrawalPayout: 1096.83, ... }
437
+
438
+ // Year-by-year depreciation schedule: straight-line, double-declining-balance, or sum-of-years-digits
439
+ calculateDepreciationSchedule(10000, 1000, 5, "straight-line");
440
+ // { totalDepreciation: 9000, schedule: [{ year: 1, depreciationExpense: 1800, accumulatedDepreciation: 1800, bookValue: 8200 }, ...] }
433
441
  ```
434
442
 
435
443
  ---
@@ -442,7 +450,8 @@ import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
442
450
  parseHar, validateSitemap, validateRobotsTxt,
443
451
  parseUserAgent, buildCacheControl, buildReferrerPolicy, parseIpv6Address, buildLinkHeader, checkSetCookieHeader, analyzeEmailHeaders,
444
452
  buildRangeHeader, parseContentRangeHeader, buildVaryHeader, parseVaryHeader, validateManifest,
445
- buildPermissionsPolicy, parsePermissionsPolicy, buildClientHints, parseClientHintHeaders } from "@utilix-tech/sdk/network";
453
+ buildPermissionsPolicy, parsePermissionsPolicy, buildClientHints, parseClientHintHeaders,
454
+ buildContentDisposition, parseContentDisposition } from "@utilix-tech/sdk/network";
446
455
 
447
456
  getStatusCode(404);
448
457
  // { code: 404, text: "Not Found", description: "...", category: "Client Error" }
@@ -533,6 +542,16 @@ buildClientHints(["Sec-CH-UA-Platform", "Sec-CH-DPR"]);
533
542
  // Decode raw Client Hints request-header text into structured values
534
543
  parseClientHintHeaders("Sec-CH-UA-Mobile: ?0");
535
544
  // { hints: [{ name: "Sec-CH-UA-Mobile", value: false, ... }], unrecognized: [] }
545
+
546
+ // Build a Content-Disposition header for a file download or inline display;
547
+ // non-ASCII filenames get an RFC 5987 filename* parameter alongside an
548
+ // ASCII-sanitized fallback
549
+ buildContentDisposition({ type: "attachment", filename: "résumé.pdf" });
550
+ // { header: 'attachment; filename="r_sum_.pdf"; filename*=UTF-8\'\'r%C3%A9sum%C3%A9.pdf', type: "attachment", filename: "résumé.pdf", filenameEncoded: true }
551
+
552
+ // Parse an existing Content-Disposition header, decoding filename* in preference to filename
553
+ parseContentDisposition('attachment; filename="report.pdf"');
554
+ // { type: "attachment", filename: "report.pdf", filenameIsExtended: false, params: { filename: "report.pdf" } }
536
555
  ```
537
556
 
538
557
  ---
@@ -692,10 +711,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
692
711
 
693
712
  ---
694
713
 
695
- ### `/media`: Image Header Parsing, EXIF, PDF Metadata & Page Dimensions, WAV, ID3, ICO, FLAC & Video Metadata
714
+ ### `/media`: Image Header Parsing, EXIF, PDF Metadata & Page Dimensions, WAV, ID3, ICO, FLAC, Video & GIF Metadata
696
715
 
697
716
  ```ts
698
- import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo } from "@utilix-tech/sdk/media";
717
+ import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo, inspectGifFrames } from "@utilix-tech/sdk/media";
699
718
 
700
719
  // Reads format, dimensions, bit depth, and alpha channel straight from
701
720
  // file bytes: no decoding, no canvas, no image library.
@@ -753,9 +772,17 @@ readFlacInfo(new Uint8Array(flacBytes));
753
772
  const videoBytes = await fs.promises.readFile("clip.mp4");
754
773
  readVideoInfo(new Uint8Array(videoBytes));
755
774
  // { format: "mp4", durationSeconds: 12.5, width: 1920, height: 1080, videoCodec: "avc1", audioCodec: "mp4a", ... }
775
+
776
+ // Walks a GIF87a/89a file's block structure (image descriptors, graphic
777
+ // control extensions, the NETSCAPE2.0 application extension) to report
778
+ // frame count, total animation duration, and loop count, without decoding
779
+ // any LZW-compressed pixel data.
780
+ const gifBytes = await fs.promises.readFile("animation.gif");
781
+ inspectGifFrames(new Uint8Array(gifBytes));
782
+ // { version: "89a", width: 480, height: 270, frameCount: 24, animated: true, loopCount: 0, totalDurationMs: 2400, ... }
756
783
  ```
757
784
 
758
- `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files. `readFlacInfo` reads the native FLAC container only, not FLAC-in-Ogg. `readVideoInfo` supports MP4 (ISO BMFF) and WebM (EBML/Matroska) containers only.
785
+ `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files. `readFlacInfo` reads the native FLAC container only, not FLAC-in-Ogg. `readVideoInfo` supports MP4 (ISO BMFF) and WebM (EBML/Matroska) containers only. `inspectGifFrames` reads the GIF87a/89a block structure only; it never decodes pixel data.
759
786
 
760
787
  ---
761
788
 
@@ -771,14 +798,14 @@ readVideoInfo(new Uint8Array(videoBytes));
771
798
  | `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, entropy estimator, fake data |
772
799
  | `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
773
800
  | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, EMI calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator, Rule of 72 calculator, savings goal calculator, ROI calculator, cash denomination/till calculator, payback period calculator, CD maturity calculator |
774
- | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand, Permissions-Policy header builder, Client Hints header builder/parser |
801
+ | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand, Permissions-Policy header builder, Client Hints header builder/parser, Content-Disposition header builder/parser |
775
802
  | `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
776
803
  | `@utilix-tech/sdk/code` | SQL format/minify, HTML format/minify, regex tester, GraphQL format, MIME type lookup, GitHub URL parsing, regex ReDoS detector, Conventional Commits linter |
777
804
  | `@utilix-tech/sdk/color` | Hex/RGB/HSL/HSV/CMYK convert, palette generation, contrast check |
778
805
  | `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
779
806
  | `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
780
807
  | `@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, vector similarity, few-shot prompt formatter, tool-definition schema linter |
781
- | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; PDF page dimension/orientation listing; WAV audio info; ID3 tags; ICO favicon inspection; FLAC metadata reading; MP4/WebM video info |
808
+ | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; PDF page dimension/orientation listing; WAV audio info; ID3 tags; ICO favicon inspection; FLAC metadata reading; MP4/WebM video info; GIF frame/loop inspection |
782
809
 
783
810
  ---
784
811
 
@@ -3,6 +3,7 @@ import { __export } from './chunk-MLKGABMK.js';
3
3
  // src/tools/media.ts
4
4
  var media_exports = {};
5
5
  __export(media_exports, {
6
+ inspectGifFrames: () => inspectGifFrames,
6
7
  readExifData: () => readExifData,
7
8
  readFlacInfo: () => readFlacInfo,
8
9
  readIcoInfo: () => readIcoInfo,
@@ -1220,5 +1221,90 @@ function parseWebm(bytes) {
1220
1221
  }
1221
1222
  return result;
1222
1223
  }
1224
+ function skipGifSubBlocks(bytes, offset) {
1225
+ while (offset < bytes.length) {
1226
+ const len = bytes[offset];
1227
+ offset += 1;
1228
+ if (len === 0) break;
1229
+ offset += len;
1230
+ }
1231
+ return offset;
1232
+ }
1233
+ function inspectGifFrames(bytes) {
1234
+ if (bytes.length < 13) return { error: "File too small to be a valid GIF file" };
1235
+ const signature = Buffer.from(bytes.subarray(0, 6)).toString("ascii");
1236
+ if (signature !== "GIF87a" && signature !== "GIF89a") return { error: "Not a valid GIF file (missing GIF87a/GIF89a signature)" };
1237
+ const version = signature.slice(3);
1238
+ const width = readU16LE(bytes, 6);
1239
+ const height = readU16LE(bytes, 8);
1240
+ const packed = bytes[10];
1241
+ const hasGlobalColorTable = (packed & 128) !== 0;
1242
+ const globalColorTableSize = hasGlobalColorTable ? 2 << (packed & 7) : 0;
1243
+ let offset = 13;
1244
+ if (hasGlobalColorTable) offset += globalColorTableSize * 3;
1245
+ let frameCount = 0;
1246
+ let loopCount = null;
1247
+ let totalDurationMs = 0;
1248
+ let hasTransparency = false;
1249
+ let pendingDelay = 0;
1250
+ while (offset < bytes.length) {
1251
+ const blockType = bytes[offset];
1252
+ if (blockType === 59) break;
1253
+ if (blockType === 33) {
1254
+ const label = bytes[offset + 1];
1255
+ offset += 2;
1256
+ if (label === 249) {
1257
+ const subLen = bytes[offset] ?? 0;
1258
+ if (subLen >= 4) {
1259
+ const flags = bytes[offset + 1];
1260
+ pendingDelay = readU16LE(bytes, offset + 2);
1261
+ if (flags & 1) hasTransparency = true;
1262
+ }
1263
+ offset += 1 + subLen;
1264
+ offset = skipGifSubBlocks(bytes, offset);
1265
+ } else if (label === 255) {
1266
+ const blockSize = bytes[offset] ?? 0;
1267
+ const appId = Buffer.from(bytes.subarray(offset + 1, offset + 1 + Math.min(blockSize, 11))).toString("ascii");
1268
+ offset += 1 + blockSize;
1269
+ if (appId === "NETSCAPE2.0") {
1270
+ const subLen = bytes[offset] ?? 0;
1271
+ if (subLen >= 3 && bytes[offset + 1] === 1) {
1272
+ loopCount = readU16LE(bytes, offset + 2);
1273
+ }
1274
+ }
1275
+ offset = skipGifSubBlocks(bytes, offset);
1276
+ } else {
1277
+ offset = skipGifSubBlocks(bytes, offset);
1278
+ }
1279
+ } else if (blockType === 44) {
1280
+ if (offset + 10 > bytes.length) return { error: "Truncated image descriptor" };
1281
+ frameCount++;
1282
+ totalDurationMs += pendingDelay * 10;
1283
+ pendingDelay = 0;
1284
+ const packedImg = bytes[offset + 9];
1285
+ const hasLocalColorTable = (packedImg & 128) !== 0;
1286
+ const localColorTableSize = hasLocalColorTable ? 2 << (packedImg & 7) : 0;
1287
+ offset += 10;
1288
+ if (hasLocalColorTable) offset += localColorTableSize * 3;
1289
+ offset += 1;
1290
+ offset = skipGifSubBlocks(bytes, offset);
1291
+ } else {
1292
+ break;
1293
+ }
1294
+ }
1295
+ if (frameCount === 0) return { error: "No image data blocks found in this GIF file" };
1296
+ return {
1297
+ version,
1298
+ width,
1299
+ height,
1300
+ frameCount,
1301
+ animated: frameCount > 1,
1302
+ loopCount,
1303
+ totalDurationMs,
1304
+ hasGlobalColorTable,
1305
+ globalColorTableSize,
1306
+ hasTransparency
1307
+ };
1308
+ }
1223
1309
 
1224
- export { media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readPdfPageDimensions, readVideoInfo, readWavInfo };
1310
+ export { inspectGifFrames, media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readPdfPageDimensions, readVideoInfo, readWavInfo };
@@ -25,6 +25,7 @@ __export(units_exports, {
25
25
  calculateCdMaturity: () => calculateCdMaturity,
26
26
  calculateChange: () => calculateChange,
27
27
  calculateCompoundInterest: () => calculateCompoundInterest,
28
+ calculateDepreciationSchedule: () => calculateDepreciationSchedule,
28
29
  calculateDiscount: () => calculateDiscount,
29
30
  calculateEmi: () => calculateEmi,
30
31
  calculateLoan: () => calculateLoan,
@@ -2022,5 +2023,58 @@ function calculateCdMaturity(principal, annualRatePercent, termMonths, compounds
2022
2023
  }
2023
2024
  return result;
2024
2025
  }
2026
+ var DEPRECIATION_METHODS = ["straight-line", "double-declining-balance", "sum-of-years-digits"];
2027
+ function calculateDepreciationSchedule(cost, salvageValue, usefulLifeYears, method) {
2028
+ if (!Number.isFinite(cost) || cost <= 0) return { error: "Cost must be a positive number" };
2029
+ if (!Number.isFinite(salvageValue) || salvageValue < 0) {
2030
+ return { error: "Salvage value must be zero or a positive number" };
2031
+ }
2032
+ if (salvageValue >= cost) return { error: "Salvage value must be less than cost" };
2033
+ if (!Number.isInteger(usefulLifeYears) || usefulLifeYears <= 0) {
2034
+ return { error: "Useful life must be a positive whole number of years" };
2035
+ }
2036
+ if (!DEPRECIATION_METHODS.includes(method)) {
2037
+ return { error: `Method must be one of: ${DEPRECIATION_METHODS.join(", ")}` };
2038
+ }
2039
+ const round22 = (n) => Math.round(n * 100) / 100;
2040
+ const depreciableBase = round22(cost - salvageValue);
2041
+ const schedule = [];
2042
+ let accumulated = 0;
2043
+ if (method === "straight-line") {
2044
+ const annual = round22(depreciableBase / usefulLifeYears);
2045
+ for (let year = 1; year <= usefulLifeYears; year++) {
2046
+ const expense = year === usefulLifeYears ? round22(depreciableBase - accumulated) : annual;
2047
+ accumulated = round22(accumulated + expense);
2048
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: round22(cost - accumulated) });
2049
+ }
2050
+ } else if (method === "double-declining-balance") {
2051
+ const rate = 2 / usefulLifeYears;
2052
+ let bookValue = cost;
2053
+ for (let year = 1; year <= usefulLifeYears; year++) {
2054
+ let expense = round22(bookValue * rate);
2055
+ if (bookValue - expense < salvageValue) expense = round22(bookValue - salvageValue);
2056
+ if (expense < 0) expense = 0;
2057
+ bookValue = round22(bookValue - expense);
2058
+ accumulated = round22(accumulated + expense);
2059
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue });
2060
+ }
2061
+ } else {
2062
+ const sumOfYears = usefulLifeYears * (usefulLifeYears + 1) / 2;
2063
+ for (let year = 1; year <= usefulLifeYears; year++) {
2064
+ const fraction = (usefulLifeYears - year + 1) / sumOfYears;
2065
+ const expense = year === usefulLifeYears ? round22(depreciableBase - accumulated) : round22(fraction * depreciableBase);
2066
+ accumulated = round22(accumulated + expense);
2067
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: round22(cost - accumulated) });
2068
+ }
2069
+ }
2070
+ return {
2071
+ cost,
2072
+ salvageValue,
2073
+ usefulLifeYears,
2074
+ method,
2075
+ totalDepreciation: accumulated,
2076
+ schedule
2077
+ };
2078
+ }
2025
2079
 
2026
- export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCashBreakdown, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
2080
+ export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCashBreakdown, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
@@ -11,6 +11,7 @@ __export(text_exports, {
11
11
  LINE_OPS: () => LINE_OPS,
12
12
  MORSE_CODE: () => MORSE_CODE,
13
13
  addBorder: () => addBorder,
14
+ analyzeKeywordDensity: () => analyzeKeywordDensity,
14
15
  analyzeSentenceLengths: () => analyzeSentenceLengths,
15
16
  analyzeText: () => analyzeText,
16
17
  applyOp: () => applyOp,
@@ -2670,6 +2671,131 @@ function analyzeSentenceLengths(text) {
2670
2671
  buckets
2671
2672
  };
2672
2673
  }
2674
+ var KEYWORD_DENSITY_STOPWORDS = /* @__PURE__ */ new Set([
2675
+ "a",
2676
+ "an",
2677
+ "the",
2678
+ "and",
2679
+ "or",
2680
+ "but",
2681
+ "if",
2682
+ "then",
2683
+ "else",
2684
+ "for",
2685
+ "nor",
2686
+ "so",
2687
+ "yet",
2688
+ "of",
2689
+ "in",
2690
+ "on",
2691
+ "at",
2692
+ "to",
2693
+ "from",
2694
+ "by",
2695
+ "with",
2696
+ "about",
2697
+ "against",
2698
+ "between",
2699
+ "into",
2700
+ "through",
2701
+ "during",
2702
+ "before",
2703
+ "after",
2704
+ "above",
2705
+ "below",
2706
+ "up",
2707
+ "down",
2708
+ "out",
2709
+ "off",
2710
+ "over",
2711
+ "under",
2712
+ "again",
2713
+ "further",
2714
+ "once",
2715
+ "is",
2716
+ "are",
2717
+ "was",
2718
+ "were",
2719
+ "be",
2720
+ "been",
2721
+ "being",
2722
+ "am",
2723
+ "do",
2724
+ "does",
2725
+ "did",
2726
+ "doing",
2727
+ "have",
2728
+ "has",
2729
+ "had",
2730
+ "having",
2731
+ "i",
2732
+ "me",
2733
+ "my",
2734
+ "we",
2735
+ "our",
2736
+ "you",
2737
+ "your",
2738
+ "he",
2739
+ "him",
2740
+ "his",
2741
+ "she",
2742
+ "her",
2743
+ "it",
2744
+ "its",
2745
+ "they",
2746
+ "them",
2747
+ "their",
2748
+ "this",
2749
+ "that",
2750
+ "these",
2751
+ "those",
2752
+ "as",
2753
+ "not",
2754
+ "no",
2755
+ "can",
2756
+ "will",
2757
+ "just",
2758
+ "than",
2759
+ "too",
2760
+ "very",
2761
+ "would",
2762
+ "should",
2763
+ "could",
2764
+ "there",
2765
+ "here",
2766
+ "what",
2767
+ "which",
2768
+ "who",
2769
+ "whom"
2770
+ ]);
2771
+ function analyzeKeywordDensity(text, options = {}) {
2772
+ if (!text || !text.trim()) return { error: "Input text is empty" };
2773
+ const topN = options.topN ?? 10;
2774
+ const minWordLength = options.minWordLength ?? 3;
2775
+ const ngramSize = options.ngramSize ?? 1;
2776
+ if (!Number.isInteger(topN) || topN <= 0) return { error: "topN must be a positive whole number" };
2777
+ if (!Number.isInteger(minWordLength) || minWordLength < 1) return { error: "minWordLength must be a positive whole number" };
2778
+ if (!Number.isInteger(ngramSize) || ngramSize < 1 || ngramSize > 3) return { error: "ngramSize must be 1, 2, or 3" };
2779
+ const tokens = text.toLowerCase().match(/[a-z0-9']+/g) ?? [];
2780
+ const totalWords = tokens.length;
2781
+ if (totalWords === 0) return { error: "No words found in input text" };
2782
+ if (totalWords < ngramSize) return { error: "Input text has fewer words than the requested phrase length" };
2783
+ const counts = /* @__PURE__ */ new Map();
2784
+ for (let i = 0; i <= tokens.length - ngramSize; i++) {
2785
+ const phraseWords = tokens.slice(i, i + ngramSize);
2786
+ if (phraseWords.some((w) => w.length < minWordLength)) continue;
2787
+ if (ngramSize === 1 && KEYWORD_DENSITY_STOPWORDS.has(phraseWords[0])) continue;
2788
+ const phrase = phraseWords.join(" ");
2789
+ counts.set(phrase, (counts.get(phrase) ?? 0) + 1);
2790
+ }
2791
+ const keywords = Array.from(counts.entries()).map(([keyword, count]) => ({ keyword, count, density: Math.round(count / totalWords * 1e3) / 10 })).sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword)).slice(0, topN);
2792
+ return {
2793
+ totalWords,
2794
+ uniqueKeywords: counts.size,
2795
+ ngramSize,
2796
+ keywords
2797
+ };
2798
+ }
2673
2799
  var EMOJI_TABLE = [
2674
2800
  { emoji: "\u{1F600}", name: "grinning face", shortcode: "grinning", codepoint: "U+1F600", category: "Smileys & Emotion", keywords: ["happy", "smile"] },
2675
2801
  { emoji: "\u{1F603}", name: "grinning face with big eyes", shortcode: "smiley", codepoint: "U+1F603", category: "Smileys & Emotion", keywords: ["happy", "joy"] },
@@ -2970,4 +3096,4 @@ function getEmojiCategories() {
2970
3096
  return Array.from(new Set(EMOJI_TABLE.map((e) => e.category)));
2971
3097
  }
2972
3098
 
2973
- export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, EMOJI_TABLE, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeSentenceLengths, analyzeText, applyOp, applyOps, calculateStringDistance, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectFillerWords, detectPassiveVoice, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getEmojiByCategory, getEmojiByChar, getEmojiByShortcode, getEmojiCategories, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, scoreReadability, searchEmoji, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
3099
+ export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, EMOJI_TABLE, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeKeywordDensity, analyzeSentenceLengths, analyzeText, applyOp, applyOps, calculateStringDistance, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectFillerWords, detectPassiveVoice, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getEmojiByCategory, getEmojiByChar, getEmojiByShortcode, getEmojiCategories, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, scoreReadability, searchEmoji, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
@@ -13,6 +13,7 @@ __export(network_exports, {
13
13
  analyzeEmailHeaders: () => analyzeEmailHeaders,
14
14
  buildCacheControl: () => buildCacheControl,
15
15
  buildClientHints: () => buildClientHints,
16
+ buildContentDisposition: () => buildContentDisposition,
16
17
  buildDnsQueryUrl: () => buildDnsQueryUrl,
17
18
  buildGeoUrl: () => buildGeoUrl,
18
19
  buildLinkHeader: () => buildLinkHeader,
@@ -37,6 +38,7 @@ __export(network_exports, {
37
38
  isValidIpv4: () => isValidIpv4,
38
39
  isValidIpv6: () => isValidIpv6,
39
40
  parseClientHintHeaders: () => parseClientHintHeaders,
41
+ parseContentDisposition: () => parseContentDisposition,
40
42
  parseContentRangeHeader: () => parseContentRangeHeader,
41
43
  parseDnsResponse: () => parseDnsResponse,
42
44
  parseGeoResponse: () => parseGeoResponse,
@@ -2678,5 +2680,52 @@ function parseClientHintHeaders(rawHeaders) {
2678
2680
  }
2679
2681
  return { hints, unrecognized };
2680
2682
  }
2683
+ function encodeRFC5987ValueChars(value) {
2684
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
2685
+ }
2686
+ function buildContentDisposition(options = {}) {
2687
+ const type = options.type ?? "attachment";
2688
+ if (type !== "inline" && type !== "attachment") return { error: 'type must be "inline" or "attachment"' };
2689
+ if (!options.filename) return { header: type, type, filenameEncoded: false };
2690
+ const filename = options.filename;
2691
+ if (filename.includes("/") || filename.includes("\\")) return { error: "filename must not contain path separators" };
2692
+ const isAscii = /^[\x20-\x7e]*$/.test(filename);
2693
+ const asciiFallback = filename.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1");
2694
+ let header = `${type}; filename="${asciiFallback}"`;
2695
+ let filenameEncoded = false;
2696
+ if (!isAscii) {
2697
+ header += `; filename*=UTF-8''${encodeRFC5987ValueChars(filename)}`;
2698
+ filenameEncoded = true;
2699
+ }
2700
+ return { header, type, filename, filenameEncoded };
2701
+ }
2702
+ function parseContentDisposition(header) {
2703
+ if (typeof header !== "string" || header.trim() === "") return { error: "header is required" };
2704
+ const parts = header.split(";").map((p) => p.trim()).filter(Boolean);
2705
+ const type = parts[0]?.toLowerCase();
2706
+ if (!type) return { error: "Could not parse a disposition type from the header" };
2707
+ const params = {};
2708
+ for (let i = 1; i < parts.length; i++) {
2709
+ const eq = parts[i].indexOf("=");
2710
+ if (eq === -1) continue;
2711
+ const key = parts[i].slice(0, eq).trim().toLowerCase();
2712
+ let value = parts[i].slice(eq + 1).trim();
2713
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
2714
+ params[key] = value;
2715
+ }
2716
+ let filename = params.filename;
2717
+ let filenameIsExtended = false;
2718
+ if (params["filename*"]) {
2719
+ const match = params["filename*"].match(/^([^']*)'([^']*)'(.*)$/);
2720
+ if (match) {
2721
+ try {
2722
+ filename = decodeURIComponent(match[3]);
2723
+ filenameIsExtended = true;
2724
+ } catch {
2725
+ }
2726
+ }
2727
+ }
2728
+ return { type, filename, filenameIsExtended, params };
2729
+ }
2681
2730
 
2682
- export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildClientHints, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildRangeHeader, buildReferrerPolicy, buildVaryHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseClientHintHeaders, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parsePermissionsPolicy, parseUserAgent, parseVaryHeader, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
2731
+ export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildClientHints, buildContentDisposition, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildRangeHeader, buildReferrerPolicy, buildVaryHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseClientHintHeaders, parseContentDisposition, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parsePermissionsPolicy, parseUserAgent, parseVaryHeader, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };