{"_id":"@azghr/shorn","_rev":"4-04f8a2f0c6f9f3db25c81bfc9f2ce18e","name":"@azghr/shorn","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"@azghr/shorn","version":"0.1.0","keywords":["truncate","string","byte","grapheme","utf8","unicode","safe","limit","budget"],"license":"MIT","_id":"@azghr/shorn@0.1.0","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"dist":{"shasum":"8aa014eaebaea34993424bcbbc90f4c674495c45","tarball":"https://registry.npmjs.org/@azghr/shorn/-/shorn-0.1.0.tgz","fileCount":9,"integrity":"sha512-OwcUQexze2MJZxc+E5DBMLitmWeSArS3aIfDSysa9/6ldKvm9ziJoVYHZETtVY3LNerJ1VRwpzEMfwlpSJNvGw==","signatures":[{"sig":"MEYCIQDCo6DJ3hkhd1bjiWcd85z3jNcgzBh1E+HeEUJejdFepAIhALWgDJ16mfWscGQCA+04vfPSbOfiwmqrWIY/fHC8pmgS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":25055},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4c4c708d079bb1234056cd8809526a6d8d5d3e2c","scripts":{"demo":"tsx examples/demo.ts","lint":"eslint src test examples","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","check":"pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm run check"},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"_npmVersion":"11.6.2","description":"Truncate strings by byte budget without ever breaking a grapheme — safe for DB columns, headers, and anything with a byte limit.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.4","tsup":"^8.4.0","eslint":"^9.29.0","vitest":"^3.2.1","@eslint/js":"^9.29.0","typescript":"^5.8.3","@types/node":"^22.16.3","typescript-eslint":"^8.34.0"},"_npmOperationalInternal":{"tmp":"tmp/shorn_0.1.0_1784299222764_0.29504759625890653","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@azghr/shorn","version":"0.1.1","keywords":["truncate","string","byte","grapheme","utf8","unicode","safe","limit","budget"],"license":"MIT","_id":"@azghr/shorn@0.1.1","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"dist":{"shasum":"2c3fb50162d03edf74727be8ef28c286e8cf5d10","tarball":"https://registry.npmjs.org/@azghr/shorn/-/shorn-0.1.1.tgz","fileCount":9,"integrity":"sha512-+noPcfMVcnXAvCYI0LgBICBgUv8bMjxZpDBBtMb/6cHsplxzBxwTTMMhy4rYizH+0Ysip+Edfd38sds+YtAXyA==","signatures":[{"sig":"MEUCIFRMQ4gf1mgQgizpWRmxXExulRy4otwqX5+4fVJ+h5YGAiEA7lsJ9GK6ibvVEFSQSeZutVtPWcY+1MkvFUj3UPPCvb8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":25584},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"12037dc5e1c2ff29f278fe9645852b66a5f40bd1","scripts":{"demo":"tsx examples/demo.ts","lint":"eslint src test examples","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","check":"pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm run check"},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"_npmVersion":"11.6.2","description":"Truncate strings by byte budget without ever breaking a grapheme — safe for DB columns, headers, and anything with a byte limit.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.4","tsup":"^8.4.0","eslint":"^9.29.0","vitest":"^3.2.1","@eslint/js":"^9.29.0","typescript":"^5.8.3","@types/node":"^22.16.3","typescript-eslint":"^8.34.0"},"_npmOperationalInternal":{"tmp":"tmp/shorn_0.1.1_1784550245238_0.1725530835103739","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@azghr/shorn","version":"0.1.2","keywords":["truncate","string","byte","grapheme","utf8","unicode","safe","limit","budget"],"license":"MIT","_id":"@azghr/shorn@0.1.2","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"dist":{"shasum":"a6ce500a6b563f05e6a18768f866890e8265bea9","tarball":"https://registry.npmjs.org/@azghr/shorn/-/shorn-0.1.2.tgz","fileCount":10,"integrity":"sha512-Dz2sYkUlvSQvBaovTpyvFNyInD/H+r9AjJNZb7n9y1CT3lMx6lE2T26i7r5pEO6KJNrGO0RifzLNevG3PZnKkQ==","signatures":[{"sig":"MEQCIEJ8BcPCe8VFqhLOP8R9y7/tIzelW2hSyZiscX0mIbQaAiAlBXM8haDGUkBFOO8g3SG4xrmnjuts3JDtBNHU9pAw4Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26383},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"0c1ead889bfa2fa1bdcaaf9a8e820a6b4de440e3","scripts":{"demo":"tsx examples/demo.ts","lint":"eslint src test examples","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","check":"pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm run check"},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"_npmVersion":"11.6.2","description":"Truncate strings by byte budget without ever breaking a grapheme — safe for DB columns, headers, and anything with a byte limit.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.4","tsup":"^8.4.0","eslint":"^9.29.0","vitest":"^3.2.1","@eslint/js":"^9.29.0","typescript":"^5.8.3","@types/node":"^22.16.3","typescript-eslint":"^8.34.0"},"_npmOperationalInternal":{"tmp":"tmp/shorn_0.1.2_1784640774619_0.14780462272846928","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@azghr/shorn","version":"0.1.3","description":"Truncate strings by byte budget without ever breaking a grapheme — safe for DB columns, headers, and anything with a byte limit.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"sideEffects":false,"engines":{"node":">=18"},"license":"MIT","keywords":["truncate","string","byte","grapheme","utf8","unicode","safe","limit","budget"],"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","lint":"eslint src test examples","demo":"tsx examples/demo.ts","check":"pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build","prepublishOnly":"pnpm run check"},"devDependencies":{"@eslint/js":"^9.29.0","@types/node":"^22.16.3","eslint":"^9.29.0","tsup":"^8.4.0","tsx":"^4.19.4","typescript":"^5.8.3","typescript-eslint":"^8.34.0","vitest":"^3.2.1"},"pnpm":{"onlyBuiltDependencies":["esbuild"]},"gitHead":"7d3bdd3d558c981596249dadffb12e4703098764","_id":"@azghr/shorn@0.1.3","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-41G19r78abCRpPYdKwtmuw+dVAM3be5qFlgijKPP9wiiyZzwExLFkQsLDXeGVSGXEELc1fEsIMA8c158ZQ+WyQ==","shasum":"191e7d3d764c61db1dbc242d3998490b97fd1e60","tarball":"https://registry.npmjs.org/@azghr/shorn/-/shorn-0.1.3.tgz","fileCount":10,"unpackedSize":27502,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBibmjIo6asu3WjzGmq0u1UBYPiFyrh3zO1SNmBRfJN0AiEA5mgV5FsSLb+SzkLd04VowQmt4103CT1ht9cde1/pof8="}]},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"directories":{},"maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/shorn_0.1.3_1784746433800_0.07627501045152951"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T14:40:22.587Z","modified":"2026-07-22T18:53:54.140Z","0.1.0":"2026-07-17T14:40:22.904Z","0.1.1":"2026-07-20T12:24:05.363Z","0.1.2":"2026-07-21T13:32:54.768Z","0.1.3":"2026-07-22T18:53:53.967Z"},"license":"MIT","keywords":["truncate","string","byte","grapheme","utf8","unicode","safe","limit","budget"],"description":"Truncate strings by byte budget without ever breaking a grapheme — safe for DB columns, headers, and anything with a byte limit.","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"readme":"# shorn\n\n[![npm](https://img.shields.io/npm/v/@azghr/shorn)](https://www.npmjs.com/package/@azghr/shorn)\n[![MIT License](https://img.shields.io/npm/l/@azghr/shorn)](LICENSE)\n\nTruncate strings by byte budget without ever breaking a grapheme cluster. Safe for database columns, HTTP headers, and anything with a hard byte limit.\n\n## The problem\n\nA MySQL `VARCHAR(255) utf8mb4` column holds up to 255 UTF-8 bytes. If you `str.slice(0, 255)` a user bio, you can land in the middle of a 4-byte emoji or a ZWJ family sequence. Encoding the sliced string back to UTF-8 and decoding it produces `U+FFFD` (replacement character) — the emoji is destroyed.\n\nThe same failure mode hits HTTP headers, MongoDB indexed fields, and any system with a byte limit on encoded text. The fix is to never split a grapheme cluster, which is what shorn guarantees.\n\n## Install\n\n```bash\nnpm install @azghr/shorn\n# or\npnpm add @azghr/shorn\n# or\nyarn add @azghr/shorn\n```\n\n## Use\n\nTruncate a string so it fits in 64 UTF-8 bytes. If it already fits, the original string reference is returned unchanged (zero alloc).\n\n```ts\nimport { shorn } from \"@azghr/shorn\";\n\nconst bio = \"Hi! I love 🎉 and 👨‍👩‍👧‍👦\";\nconst safe = shorn(bio, 64);\n// safe is 42 bytes, contains whole graphemes only\n```\n\nTruncate with an ellipsis counted inside the budget:\n\n```ts\nconst longBio = \"Hi! I love 🎉 and 👨‍👩‍👧‍👦 and also 🌍 café 🇩🇪\";\nconst safe = shorn(longBio, 50, { unit: \"utf8\", ellipsis: \"...\" });\n// safe is at most 50 bytes, ends with \"...\"\n```\n\nMeasure in UTF-16 code units or grapheme clusters instead of bytes:\n\n```ts\nshorn(bio, 30, { unit: \"utf16\" });                 // 30 code units\nshorn(bio, 10, { unit: \"grapheme\" });               // 10 grapheme clusters\nshorn(bio, 10, { unit: \"grapheme\", ellipsis: \"…\" }); // 10 grapheme clusters + ellipsis\n```\n\n## API\n\n### `shorn(input, max, options?)`\n\n| Param     | Type     | Default    | Description |\n|-----------|----------|------------|-------------|\n| `input`   | `string` | (required) | String to truncate |\n| `max`     | `number` | (required) | Maximum size in the chosen unit. Must be a non-negative integer |\n| `options` | `ShornOptions` | `{}` | See below |\n\n**Returns**: `string` -- the longest prefix of whole grapheme clusters whose measured size does not exceed `max`, optionally followed by the ellipsis. If the input already fits, returns the **same reference** (identity fast path).\n\n**Throws**: `TypeError` if `max < 0`, `NaN`, `Infinity`, or not an integer.\n\n### `ShornOptions`\n\n```ts\ninterface ShornOptions {\n  unit?: \"utf8\" | \"utf16\" | \"grapheme\";  // default \"utf8\"\n  ellipsis?: string;                       // default \"\" (none)\n  locale?: string;                         // passed to Intl.Segmenter\n}\n```\n\n- **`unit`**: Measurement mode. `\"utf8\"` uses `TextEncoder` byte length. `\"utf16\"` uses `string.length` (code units). `\"grapheme\"` uses `Intl.Segmenter` with `granularity: \"grapheme\"`.\n- **`ellipsis`**: Appended to the result when truncation occurs. Measured in the same `unit` and counted **inside** the budget. If not even one grapheme can fit alongside the ellipsis, the ellipsis is dropped and only the grapheme prefix is returned. If the ellipsis alone exceeds the budget, it is dropped.\n- **`locale`**: BCP-47 locale forwarded to `Intl.Segmenter`. Only affects grapheme segmentation rules (e.g. locale-specific cluster boundaries in Indic scripts). Default `undefined` (runtime default).\n\n## Non-goals\n\nThis package will never support:\n\n- Streaming / chunked truncation\n- Unicode normalization (NFC/NFD/NFKC/NFKD)\n- Custom segmenter rules -- only `Intl.Segmenter` with `\"grapheme\"` granularity\n- Word-boundary or sentence-boundary truncation\n- In-place mutation\n- Non-UTF encodings (GB18030, Shift_JIS, etc.)\n- Surrogate-pair healing (grapheme segmentation already handles this)\n- Configurable normalization of the ellipsis\n- Terminal column width measurement\n- Padding\n\n**Composition example**: If you need word-boundary truncation, pair shorn with a word splitter:\n\n```ts\nimport { shorn } from \"@azghr/shorn\";\n\nfunction truncateWords(text: string, maxBytes: number): string {\n  const words = text.split(\" \");\n  let candidate = \"\";\n  for (const word of words) {\n    const next = candidate ? candidate + \" \" + word : word;\n    if (shorn(next, maxBytes).length !== next.length) break;\n    candidate = next;\n  }\n  return candidate;\n}\n```\n\n## Related Packages\n\n**Caching & Concurrency:**\n- **[@azghr/filterkit](https://www.npmjs.com/package/@azghr/filterkit)** — Framework-agnostic, type-safe filtering for TypeScript\n- **[@azghr/singlet](https://www.npmjs.com/package/@azghr/singlet)** — Deduplicate concurrent async calls\n- **[staleness](https://www.npmjs.com/package/staleness)** — Stale-while-revalidate caching for async functions\n\n**Text Processing:**\n- **[seriatim](https://www.npmjs.com/package/seriatim)** — Sequential processing utilities\n\n**HTTP & Network:**\n- **[forbear](https://www.npmjs.com/package/forbear)** — Read server rate-limit instructions from HTTP responses\n- **[forestall](https://www.npmjs.com/package/forestall)** — Delay execution until a condition is met\n- **[obviate](https://www.npmjs.com/package/obviate)** — Render operations unnecessary through caching\n\n**System & Process:**\n- **[quiesce](https://www.npmjs.com/package/quiesce)** — Ordered, timeboxed graceful shutdown for Node\n- **[sortition](https://www.npmjs.com/package/sortition)** — Deterministic percentage rollouts and A/B bucketing\n- **[stanch](https://www.npmjs.com/package/stanch)** — Stop flows or operations based on conditions\n\n**Utilities:**\n- **[expunge](https://www.npmjs.com/package/expunge)** — Remove or exclude items from collections\n- **[occlude](https://www.npmjs.com/package/occlude)** — Hide or mask data and functionality\n- **[placemark](https://www.npmjs.com/package/placemark)** — Geographic location and mapping utilities\n- **[specie](https://www.npmjs.com/package/specie)** — Currency and financial calculations\n\n## License\n\nMIT\n","readmeFilename":"README.md"}