@demicodes/utils 0.13.0 → 0.14.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.
package/dist/index.d.mts CHANGED
@@ -90,6 +90,23 @@ declare function base64ToBytes(base64: string): Uint8Array;
90
90
  declare function clamp(value: number, min: number, max: number): number;
91
91
  /** Strips trailing slashes from a base URL so paths can be appended consistently. */
92
92
  declare function normalizeBaseUrl(baseUrl: string): string;
93
+ /**
94
+ * Slices the first `maxChars` UTF-16 units of `text` without splitting a
95
+ * surrogate pair: a cut that would leave a trailing lone high surrogate moves
96
+ * back one unit instead.
97
+ */
98
+ declare function sliceHead(text: string, maxChars: number): string;
99
+ /**
100
+ * Slices the last `maxChars` UTF-16 units of `text` without splitting a
101
+ * surrogate pair: a cut that would leave a leading lone low surrogate moves
102
+ * forward one unit instead.
103
+ */
104
+ declare function sliceTail(text: string, maxChars: number): string;
105
+ /**
106
+ * Replaces lone UTF-16 surrogates with U+FFFD so the string is well-formed
107
+ * Unicode and can be serialized into any wire payload.
108
+ */
109
+ declare function toWellFormedText(text: string): string;
93
110
  /** Truncates `text` to at most `maxChars` characters, appending `ellipsis` when shortened. */
94
111
  declare function truncate(text: string, maxChars: number, ellipsis?: string): string;
95
112
  /** Returns the last `maxChars` characters of `text`. */
@@ -137,4 +154,4 @@ declare function isAbsolutePath(path: string): boolean;
137
154
  /** Generates a random UUID. */
138
155
  declare function createId(): string;
139
156
  //#endregion
140
- export { AbortError, Deferred, PortableJsonValue, abortable, asError, asRecord, asString, base64ToBytes, bytesToBase64, clamp, concatBytes, createId, decodeLatin1, decodeUtf8, decodeUtf8Strict, deferred, delay, dirnamePath, encodeLatin1, encodeUtf8, errorCode, errorMessage, isAbortError, isAbsolutePath, isFileNotFoundError, isRecord, nonEmptyString, noop, normalizeBaseUrl, normalizePath, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, parsePortableJson, safeJsonStringify, shortHash, stringOrNull, stringifyPortableJson, tail, throwIfAborted, truncate, utf8Bytes, utf8Slice, waitFor, withTimeout };
157
+ export { AbortError, Deferred, PortableJsonValue, abortable, asError, asRecord, asString, base64ToBytes, bytesToBase64, clamp, concatBytes, createId, decodeLatin1, decodeUtf8, decodeUtf8Strict, deferred, delay, dirnamePath, encodeLatin1, encodeUtf8, errorCode, errorMessage, isAbortError, isAbsolutePath, isFileNotFoundError, isRecord, nonEmptyString, noop, normalizeBaseUrl, normalizePath, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, parsePortableJson, safeJsonStringify, shortHash, sliceHead, sliceTail, stringOrNull, stringifyPortableJson, tail, throwIfAborted, toWellFormedText, truncate, utf8Bytes, utf8Slice, waitFor, withTimeout };
package/dist/index.mjs CHANGED
@@ -248,15 +248,46 @@ function clamp(value, min, max) {
248
248
  function normalizeBaseUrl(baseUrl) {
249
249
  return baseUrl.replace(/\/+$/, "");
250
250
  }
251
+ /**
252
+ * Slices the first `maxChars` UTF-16 units of `text` without splitting a
253
+ * surrogate pair: a cut that would leave a trailing lone high surrogate moves
254
+ * back one unit instead.
255
+ */
256
+ function sliceHead(text, maxChars) {
257
+ if (maxChars <= 0) return "";
258
+ if (text.length <= maxChars) return text;
259
+ const cut = text.charCodeAt(maxChars - 1);
260
+ return text.slice(0, cut >= 55296 && cut <= 56319 ? maxChars - 1 : maxChars);
261
+ }
262
+ /**
263
+ * Slices the last `maxChars` UTF-16 units of `text` without splitting a
264
+ * surrogate pair: a cut that would leave a leading lone low surrogate moves
265
+ * forward one unit instead.
266
+ */
267
+ function sliceTail(text, maxChars) {
268
+ if (maxChars <= 0) return "";
269
+ if (text.length <= maxChars) return text;
270
+ const start = text.length - maxChars;
271
+ const cut = text.charCodeAt(start);
272
+ return text.slice(cut >= 56320 && cut <= 57343 ? start + 1 : start);
273
+ }
274
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
275
+ /**
276
+ * Replaces lone UTF-16 surrogates with U+FFFD so the string is well-formed
277
+ * Unicode and can be serialized into any wire payload.
278
+ */
279
+ function toWellFormedText(text) {
280
+ return text.replace(LONE_SURROGATE, "�");
281
+ }
251
282
  /** Truncates `text` to at most `maxChars` characters, appending `ellipsis` when shortened. */
252
283
  function truncate(text, maxChars, ellipsis = "…") {
253
284
  if (text.length <= maxChars) return text;
254
- if (maxChars <= ellipsis.length) return text.slice(0, Math.max(0, maxChars));
255
- return text.slice(0, maxChars - ellipsis.length) + ellipsis;
285
+ if (maxChars <= ellipsis.length) return sliceHead(text, maxChars);
286
+ return sliceHead(text, maxChars - ellipsis.length) + ellipsis;
256
287
  }
257
288
  /** Returns the last `maxChars` characters of `text`. */
258
289
  function tail(text, maxChars) {
259
- return text.length <= maxChars ? text : text.slice(text.length - maxChars);
290
+ return sliceTail(text, maxChars);
260
291
  }
261
292
  /** A short, stable hex hash of a string (32-bit FNV-1a). Not cryptographic. */
262
293
  function shortHash(value) {
@@ -389,4 +420,4 @@ function createId() {
389
420
  return globalThis.crypto.randomUUID();
390
421
  }
391
422
  //#endregion
392
- export { AbortError, abortable, asError, asRecord, asString, base64ToBytes, bytesToBase64, clamp, concatBytes, createId, decodeLatin1, decodeUtf8, decodeUtf8Strict, deferred, delay, dirnamePath, encodeLatin1, encodeUtf8, errorCode, errorMessage, isAbortError, isAbsolutePath, isFileNotFoundError, isRecord, nonEmptyString, noop, normalizeBaseUrl, normalizePath, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, parsePortableJson, safeJsonStringify, shortHash, stringOrNull, stringifyPortableJson, tail, throwIfAborted, truncate, utf8Bytes, utf8Slice, waitFor, withTimeout };
423
+ export { AbortError, abortable, asError, asRecord, asString, base64ToBytes, bytesToBase64, clamp, concatBytes, createId, decodeLatin1, decodeUtf8, decodeUtf8Strict, deferred, delay, dirnamePath, encodeLatin1, encodeUtf8, errorCode, errorMessage, isAbortError, isAbsolutePath, isFileNotFoundError, isRecord, nonEmptyString, noop, normalizeBaseUrl, normalizePath, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, parsePortableJson, safeJsonStringify, shortHash, sliceHead, sliceTail, stringOrNull, stringifyPortableJson, tail, throwIfAborted, toWellFormedText, truncate, utf8Bytes, utf8Slice, waitFor, withTimeout };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/utils",
3
3
  "description": "Shared zero-dependency utilities for Demi (type guards, errors, async, bytes, strings, paths, JSON).",
4
- "version": "0.13.0",
4
+ "version": "0.14.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./dist/index.mjs",