@tldraw/utils 5.4.0-next.569ee2e27e8b → 5.4.0-next.591510702314

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.
@@ -795,6 +795,16 @@ export declare function isNonNull<T>(value: T): value is typeof value extends nu
795
795
  */
796
796
  export declare function isNonNullish<T>(value: T): value is typeof value extends undefined ? never : typeof value extends null ? never : T;
797
797
 
798
+ /**
799
+ * Iterate over the grapheme clusters of a string — user-perceived characters such as emoji
800
+ * sequences, flags, and accented letters — instead of UTF-16 code units. Uses `Intl.Segmenter`
801
+ * where available and falls back to iterating by code point on browsers that lack it, where a
802
+ * multi-code-point cluster is yielded as its component code points rather than as one segment.
803
+ *
804
+ * @public
805
+ */
806
+ export declare function iterateGraphemes(str: string): IterableIterator<string>;
807
+
798
808
  /**
799
809
  * A type representing a JSON array containing any valid JSON values.
800
810
  * Arrays can contain mixed types of JSON values including nested arrays and objects.
package/dist-cjs/index.js CHANGED
@@ -89,6 +89,7 @@ __export(index_exports, {
89
89
  isNativeStructuredClone: () => import_value.isNativeStructuredClone,
90
90
  isNonNull: () => import_value.isNonNull,
91
91
  isNonNullish: () => import_value.isNonNullish,
92
+ iterateGraphemes: () => import_string.iterateGraphemes,
92
93
  last: () => import_array.last,
93
94
  lerp: () => import_number.lerp,
94
95
  lns: () => import_hash.lns,
@@ -173,7 +174,7 @@ var import_version2 = require("./lib/version");
173
174
  var import_warn = require("./lib/warn");
174
175
  (0, import_version.registerTldrawLibraryVersion)(
175
176
  "@tldraw/utils",
176
- "5.4.0-next.569ee2e27e8b",
177
+ "5.4.0-next.591510702314",
177
178
  "cjs"
178
179
  );
179
180
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { getFirstCharacter } from './lib/string'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA6C;AAE7C,oBAAmC;AACnC,IAAAA,iBAAuC;AACvC,IAAAA,iBAAoC;AACpC,IAAAA,iBAAgC;AAChC,mBAUO;AACP,kBAAqB;AACrB,mBAA0B;AAC1B,qBASO;AACP,sBAAyB;AACzB,mBAA0E;AAC1E,4BAA+B;AAC/B,kBAA4B;AAC5B,sBAAyC;AACzC,kBAA0E;AAC1E,gBAAwD;AACxD,sBAAqC;AACrC,sBAAyB;AAEzB,mBAMO;AACP,iBAA2B;AAC3B,qBAA6B;AAC7B,oBAA6C;AAC7C,oBAeO;AACP,kBAA2E;AAC3E,gCAAmC;AACnC,wBAaO;AACP,mBAAsB;AACtB,kBAAyB;AACzB,qBASO;AACP,oBAAkC;AAClC,wBAA2B;AAC3B,sBAA+D;AAC/D,oBAAuB;AAQvB,iBAA6B;AAC7B,mBAOO;AACP,IAAAC,kBAA6C;AAC7C,kBAA+C;AAAA,IAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { getFirstCharacter, iterateGraphemes } from './lib/string'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA6C;AAE7C,oBAAmC;AACnC,IAAAA,iBAAuC;AACvC,IAAAA,iBAAoC;AACpC,IAAAA,iBAAgC;AAChC,mBAUO;AACP,kBAAqB;AACrB,mBAA0B;AAC1B,qBASO;AACP,sBAAyB;AACzB,mBAA0E;AAC1E,4BAA+B;AAC/B,kBAA4B;AAC5B,sBAAyC;AACzC,kBAA0E;AAC1E,gBAAwD;AACxD,sBAAqC;AACrC,sBAAyB;AAEzB,mBAMO;AACP,iBAA2B;AAC3B,qBAA6B;AAC7B,oBAA6C;AAC7C,oBAeO;AACP,kBAA2E;AAC3E,gCAAmC;AACnC,wBAaO;AACP,mBAAsB;AACtB,kBAAyB;AACzB,qBASO;AACP,oBAAoD;AACpD,wBAA2B;AAC3B,sBAA+D;AAC/D,oBAAuB;AAQvB,iBAA6B;AAC7B,mBAOO;AACP,IAAAC,kBAA6C;AAC7C,kBAA+C;AAAA,IAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": ["import_lodash", "import_version"]
7
7
  }
@@ -18,14 +18,32 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var string_exports = {};
20
20
  __export(string_exports, {
21
- getFirstCharacter: () => getFirstCharacter
21
+ getFirstCharacter: () => getFirstCharacter,
22
+ iterateGraphemes: () => iterateGraphemes
22
23
  });
23
24
  module.exports = __toCommonJS(string_exports);
24
25
  let graphemeSegmenter;
26
+ let checkedForSegmenter = false;
27
+ function getGraphemeSegmenter() {
28
+ if (!checkedForSegmenter) {
29
+ checkedForSegmenter = true;
30
+ if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
31
+ graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
32
+ }
33
+ }
34
+ return graphemeSegmenter;
35
+ }
36
+ function* iterateGraphemes(str) {
37
+ const segmenter = getGraphemeSegmenter();
38
+ if (segmenter) {
39
+ for (const { segment } of segmenter.segment(str)) yield segment;
40
+ } else {
41
+ yield* str;
42
+ }
43
+ }
25
44
  function getFirstCharacter(str) {
26
45
  if (!str) return "";
27
- graphemeSegmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
28
- for (const { segment } of graphemeSegmenter.segment(str)) {
46
+ for (const segment of iterateGraphemes(str)) {
29
47
  return segment;
30
48
  }
31
49
  return "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/string.ts"],
4
- "sourcesContent": ["let graphemeSegmenter: Intl.Segmenter | undefined\n\n/**\n * Get the first character of a string, treating a multi-code-unit character such as an emoji as a\n * single character. Unlike `str[0]` or `str.charAt(0)`, which return one UTF-16 code unit and so\n * split an emoji into a broken half, this returns the first whole grapheme cluster. Returns an\n * empty string when the input is empty.\n *\n * @example\n * ```ts\n * getFirstCharacter('hello') // 'h'\n * getFirstCharacter('\uD83D\uDE00 hello') // '\uD83D\uDE00'\n * getFirstCharacter('') // ''\n * ```\n *\n * @public\n */\nexport function getFirstCharacter(str: string): string {\n\tif (!str) return ''\n\tgraphemeSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' })\n\tfor (const { segment } of graphemeSegmenter.segment(str)) {\n\t\treturn segment\n\t}\n\treturn ''\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAI;AAiBG,SAAS,kBAAkB,KAAqB;AACtD,MAAI,CAAC,IAAK,QAAO;AACjB,wBAAsB,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC;AAC/E,aAAW,EAAE,QAAQ,KAAK,kBAAkB,QAAQ,GAAG,GAAG;AACzD,WAAO;AAAA,EACR;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["let graphemeSegmenter: Intl.Segmenter | undefined\nlet checkedForSegmenter = false\n\n/**\n * Resolve a shared grapheme `Intl.Segmenter`, or `undefined` when the runtime has no\n * `Intl.Segmenter`. `Intl.Segmenter` is unavailable on some older browsers (notably Firefox before\n * 125), so feature-detect it once rather than constructing it eagerly, which would throw at module\n * load and take down the whole app.\n */\nfunction getGraphemeSegmenter(): Intl.Segmenter | undefined {\n\tif (!checkedForSegmenter) {\n\t\tcheckedForSegmenter = true\n\t\tif (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n\t\t\tgraphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })\n\t\t}\n\t}\n\treturn graphemeSegmenter\n}\n\n/**\n * Iterate over the grapheme clusters of a string \u2014 user-perceived characters such as emoji\n * sequences, flags, and accented letters \u2014 instead of UTF-16 code units. Uses `Intl.Segmenter`\n * where available and falls back to iterating by code point on browsers that lack it, where a\n * multi-code-point cluster is yielded as its component code points rather than as one segment.\n *\n * @public\n */\nexport function* iterateGraphemes(str: string): IterableIterator<string> {\n\tconst segmenter = getGraphemeSegmenter()\n\tif (segmenter) {\n\t\tfor (const { segment } of segmenter.segment(str)) yield segment\n\t} else {\n\t\tyield* str\n\t}\n}\n\n/**\n * Get the first character of a string, treating a multi-code-unit character such as an emoji as a\n * single character. Unlike `str[0]` or `str.charAt(0)`, which return one UTF-16 code unit and so\n * split an emoji into a broken half, this returns the first whole grapheme cluster. Returns an\n * empty string when the input is empty.\n *\n * @example\n * ```ts\n * getFirstCharacter('hello') // 'h'\n * getFirstCharacter('\uD83D\uDE00 hello') // '\uD83D\uDE00'\n * getFirstCharacter('') // ''\n * ```\n *\n * @public\n */\nexport function getFirstCharacter(str: string): string {\n\tif (!str) return ''\n\tfor (const segment of iterateGraphemes(str)) {\n\t\treturn segment\n\t}\n\treturn ''\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAI;AACJ,IAAI,sBAAsB;AAQ1B,SAAS,uBAAmD;AAC3D,MAAI,CAAC,qBAAqB;AACzB,0BAAsB;AACtB,QAAI,OAAO,SAAS,eAAe,eAAe,MAAM;AACvD,0BAAoB,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC;AAAA,IAC9E;AAAA,EACD;AACA,SAAO;AACR;AAUO,UAAU,iBAAiB,KAAuC;AACxE,QAAM,YAAY,qBAAqB;AACvC,MAAI,WAAW;AACd,eAAW,EAAE,QAAQ,KAAK,UAAU,QAAQ,GAAG,EAAG,OAAM;AAAA,EACzD,OAAO;AACN,WAAO;AAAA,EACR;AACD;AAiBO,SAAS,kBAAkB,KAAqB;AACtD,MAAI,CAAC,IAAK,QAAO;AACjB,aAAW,WAAW,iBAAiB,GAAG,GAAG;AAC5C,WAAO;AAAA,EACR;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -795,6 +795,16 @@ export declare function isNonNull<T>(value: T): value is typeof value extends nu
795
795
  */
796
796
  export declare function isNonNullish<T>(value: T): value is typeof value extends undefined ? never : typeof value extends null ? never : T;
797
797
 
798
+ /**
799
+ * Iterate over the grapheme clusters of a string — user-perceived characters such as emoji
800
+ * sequences, flags, and accented letters — instead of UTF-16 code units. Uses `Intl.Segmenter`
801
+ * where available and falls back to iterating by code point on browsers that lack it, where a
802
+ * multi-code-point cluster is yielded as its component code points rather than as one segment.
803
+ *
804
+ * @public
805
+ */
806
+ export declare function iterateGraphemes(str: string): IterableIterator<string>;
807
+
798
808
  /**
799
809
  * A type representing a JSON array containing any valid JSON values.
800
810
  * Arrays can contain mixed types of JSON values including nested arrays and objects.
@@ -86,7 +86,7 @@ import {
86
86
  setInLocalStorage,
87
87
  setInSessionStorage
88
88
  } from "./lib/storage.mjs";
89
- import { getFirstCharacter } from "./lib/string.mjs";
89
+ import { getFirstCharacter, iterateGraphemes } from "./lib/string.mjs";
90
90
  import { stringEnum } from "./lib/stringEnum.mjs";
91
91
  import { FpsScheduler, fpsThrottle, throttleToNextFrame } from "./lib/throttle.mjs";
92
92
  import { Timers } from "./lib/timers.mjs";
@@ -103,7 +103,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
103
103
  import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
104
104
  registerTldrawLibraryVersion(
105
105
  "@tldraw/utils",
106
- "5.4.0-next.569ee2e27e8b",
106
+ "5.4.0-next.591510702314",
107
107
  "esm"
108
108
  );
109
109
  export {
@@ -168,6 +168,7 @@ export {
168
168
  isNativeStructuredClone,
169
169
  isNonNull,
170
170
  isNonNullish,
171
+ iterateGraphemes,
171
172
  last,
172
173
  lerp,
173
174
  lns,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { getFirstCharacter } from './lib/string'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": "AAAA,SAAS,oCAAoC;AAE7C,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA8B;AACvC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAuB;AAChC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGM;AACP,SAAS,gBAAgB;AACzB,SAAS,eAAe,2BAAkD;AAC1E,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,MAAM,0BAA0B;AACzC,SAAS,kBAAkB,kBAAkB,kBAAkB,WAAW;AAC1E,SAAS,cAAc,iBAAiB,gBAAgB;AACxD,SAAS,4BAA4B;AACrC,SAAS,gBAAgB;AAEzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,OAAO,aAAa;AAC7B,SAAS,SAAS,MAAM,UAAU,WAAW;AAC7C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,wBAAwB,mBAAmB,uBAAuB;AAC3E,SAAS,0BAA0B;AACnC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa,2BAA2B;AAC/D,SAAS,cAAc;AAQvB,SAAS,oBAAoB;AAC7B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,gCAAAC,qCAAoC;AAC7C,SAAS,sBAAsB,gBAAgB;AAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { getFirstCharacter, iterateGraphemes } from './lib/string'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": "AAAA,SAAS,oCAAoC;AAE7C,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA8B;AACvC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAuB;AAChC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGM;AACP,SAAS,gBAAgB;AACzB,SAAS,eAAe,2BAAkD;AAC1E,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,MAAM,0BAA0B;AACzC,SAAS,kBAAkB,kBAAkB,kBAAkB,WAAW;AAC1E,SAAS,cAAc,iBAAiB,gBAAgB;AACxD,SAAS,4BAA4B;AACrC,SAAS,gBAAgB;AAEzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,OAAO,aAAa;AAC7B,SAAS,SAAS,MAAM,UAAU,WAAW;AAC7C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,wBAAwB,mBAAmB,uBAAuB;AAC3E,SAAS,0BAA0B;AACnC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAAmB,wBAAwB;AACpD,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa,2BAA2B;AAC/D,SAAS,cAAc;AAQvB,SAAS,oBAAoB;AAC7B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,gCAAAC,qCAAoC;AAC7C,SAAS,sBAAsB,gBAAgB;AAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": ["default", "registerTldrawLibraryVersion"]
7
7
  }
@@ -1,13 +1,31 @@
1
1
  let graphemeSegmenter;
2
+ let checkedForSegmenter = false;
3
+ function getGraphemeSegmenter() {
4
+ if (!checkedForSegmenter) {
5
+ checkedForSegmenter = true;
6
+ if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
7
+ graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8
+ }
9
+ }
10
+ return graphemeSegmenter;
11
+ }
12
+ function* iterateGraphemes(str) {
13
+ const segmenter = getGraphemeSegmenter();
14
+ if (segmenter) {
15
+ for (const { segment } of segmenter.segment(str)) yield segment;
16
+ } else {
17
+ yield* str;
18
+ }
19
+ }
2
20
  function getFirstCharacter(str) {
3
21
  if (!str) return "";
4
- graphemeSegmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
5
- for (const { segment } of graphemeSegmenter.segment(str)) {
22
+ for (const segment of iterateGraphemes(str)) {
6
23
  return segment;
7
24
  }
8
25
  return "";
9
26
  }
10
27
  export {
11
- getFirstCharacter
28
+ getFirstCharacter,
29
+ iterateGraphemes
12
30
  };
13
31
  //# sourceMappingURL=string.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/string.ts"],
4
- "sourcesContent": ["let graphemeSegmenter: Intl.Segmenter | undefined\n\n/**\n * Get the first character of a string, treating a multi-code-unit character such as an emoji as a\n * single character. Unlike `str[0]` or `str.charAt(0)`, which return one UTF-16 code unit and so\n * split an emoji into a broken half, this returns the first whole grapheme cluster. Returns an\n * empty string when the input is empty.\n *\n * @example\n * ```ts\n * getFirstCharacter('hello') // 'h'\n * getFirstCharacter('\uD83D\uDE00 hello') // '\uD83D\uDE00'\n * getFirstCharacter('') // ''\n * ```\n *\n * @public\n */\nexport function getFirstCharacter(str: string): string {\n\tif (!str) return ''\n\tgraphemeSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' })\n\tfor (const { segment } of graphemeSegmenter.segment(str)) {\n\t\treturn segment\n\t}\n\treturn ''\n}\n"],
5
- "mappings": "AAAA,IAAI;AAiBG,SAAS,kBAAkB,KAAqB;AACtD,MAAI,CAAC,IAAK,QAAO;AACjB,wBAAsB,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC;AAC/E,aAAW,EAAE,QAAQ,KAAK,kBAAkB,QAAQ,GAAG,GAAG;AACzD,WAAO;AAAA,EACR;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["let graphemeSegmenter: Intl.Segmenter | undefined\nlet checkedForSegmenter = false\n\n/**\n * Resolve a shared grapheme `Intl.Segmenter`, or `undefined` when the runtime has no\n * `Intl.Segmenter`. `Intl.Segmenter` is unavailable on some older browsers (notably Firefox before\n * 125), so feature-detect it once rather than constructing it eagerly, which would throw at module\n * load and take down the whole app.\n */\nfunction getGraphemeSegmenter(): Intl.Segmenter | undefined {\n\tif (!checkedForSegmenter) {\n\t\tcheckedForSegmenter = true\n\t\tif (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n\t\t\tgraphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })\n\t\t}\n\t}\n\treturn graphemeSegmenter\n}\n\n/**\n * Iterate over the grapheme clusters of a string \u2014 user-perceived characters such as emoji\n * sequences, flags, and accented letters \u2014 instead of UTF-16 code units. Uses `Intl.Segmenter`\n * where available and falls back to iterating by code point on browsers that lack it, where a\n * multi-code-point cluster is yielded as its component code points rather than as one segment.\n *\n * @public\n */\nexport function* iterateGraphemes(str: string): IterableIterator<string> {\n\tconst segmenter = getGraphemeSegmenter()\n\tif (segmenter) {\n\t\tfor (const { segment } of segmenter.segment(str)) yield segment\n\t} else {\n\t\tyield* str\n\t}\n}\n\n/**\n * Get the first character of a string, treating a multi-code-unit character such as an emoji as a\n * single character. Unlike `str[0]` or `str.charAt(0)`, which return one UTF-16 code unit and so\n * split an emoji into a broken half, this returns the first whole grapheme cluster. Returns an\n * empty string when the input is empty.\n *\n * @example\n * ```ts\n * getFirstCharacter('hello') // 'h'\n * getFirstCharacter('\uD83D\uDE00 hello') // '\uD83D\uDE00'\n * getFirstCharacter('') // ''\n * ```\n *\n * @public\n */\nexport function getFirstCharacter(str: string): string {\n\tif (!str) return ''\n\tfor (const segment of iterateGraphemes(str)) {\n\t\treturn segment\n\t}\n\treturn ''\n}\n"],
5
+ "mappings": "AAAA,IAAI;AACJ,IAAI,sBAAsB;AAQ1B,SAAS,uBAAmD;AAC3D,MAAI,CAAC,qBAAqB;AACzB,0BAAsB;AACtB,QAAI,OAAO,SAAS,eAAe,eAAe,MAAM;AACvD,0BAAoB,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC;AAAA,IAC9E;AAAA,EACD;AACA,SAAO;AACR;AAUO,UAAU,iBAAiB,KAAuC;AACxE,QAAM,YAAY,qBAAqB;AACvC,MAAI,WAAW;AACd,eAAW,EAAE,QAAQ,KAAK,UAAU,QAAQ,GAAG,EAAG,OAAM;AAAA,EACzD,OAAO;AACN,WAAO;AAAA,EACR;AACD;AAiBO,SAAS,kBAAkB,KAAqB;AACtD,MAAI,CAAC,IAAK,QAAO;AACjB,aAAW,WAAW,iBAAiB,GAAG,GAAG;AAC5C,WAAO;AAAA,EACR;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "@tldraw/utils",
3
3
  "description": "tldraw infinite canvas SDK (private utilities).",
4
- "version": "5.4.0-next.569ee2e27e8b",
4
+ "version": "5.4.0-next.591510702314",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
8
8
  },
9
9
  "homepage": "https://tldraw.dev",
10
10
  "license": "SEE LICENSE IN LICENSE.md",
11
+ "tldraw_product": {
12
+ "stableId": "tldraw:sdk-core",
13
+ "name": "SDK",
14
+ "type": "product",
15
+ "premium": false
16
+ },
11
17
  "repository": {
12
18
  "type": "git",
13
19
  "url": "https://github.com/tldraw/tldraw"
package/src/index.ts CHANGED
@@ -91,7 +91,7 @@ export {
91
91
  setInLocalStorage,
92
92
  setInSessionStorage,
93
93
  } from './lib/storage'
94
- export { getFirstCharacter } from './lib/string'
94
+ export { getFirstCharacter, iterateGraphemes } from './lib/string'
95
95
  export { stringEnum } from './lib/stringEnum'
96
96
  export { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'
97
97
  export { Timers } from './lib/timers'
@@ -1,4 +1,5 @@
1
- import { getFirstCharacter } from './string'
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { getFirstCharacter, iterateGraphemes } from './string'
2
3
 
3
4
  describe('getFirstCharacter', () => {
4
5
  it('returns the first character of a plain string', () => {
@@ -21,3 +22,59 @@ describe('getFirstCharacter', () => {
21
22
  expect(getFirstCharacter(' hi')).toBe(' ')
22
23
  })
23
24
  })
25
+
26
+ describe('iterateGraphemes', () => {
27
+ it('yields each grapheme cluster in order', () => {
28
+ expect([...iterateGraphemes('a😀b')]).toEqual(['a', '😀', 'b'])
29
+ })
30
+
31
+ it('keeps a multi-code-point cluster whole', () => {
32
+ expect([...iterateGraphemes('👨‍👩‍👧!')]).toEqual(['👨‍👩‍👧', '!'])
33
+ })
34
+
35
+ it('yields nothing for an empty string', () => {
36
+ expect([...iterateGraphemes('')]).toEqual([])
37
+ })
38
+ })
39
+
40
+ // On browsers without `Intl.Segmenter` (e.g. Firefox before 125) the module must not throw and must
41
+ // still iterate; it falls back to code-point iteration. The segmenter is probed lazily on first
42
+ // use, so `Intl.Segmenter` has to be absent at call time — reset the module and remove the global
43
+ // for the duration of each call.
44
+ describe('grapheme iteration without Intl.Segmenter', () => {
45
+ afterEach(() => {
46
+ vi.resetModules()
47
+ })
48
+
49
+ async function withoutSegmenter<T>(fn: (mod: typeof import('./string')) => T): Promise<T> {
50
+ vi.resetModules()
51
+ // `Intl.Segmenter` is typed read-only; take a mutable view so we can remove and restore it.
52
+ const mutableIntl = Intl as { Segmenter?: typeof Intl.Segmenter }
53
+ const original = mutableIntl.Segmenter
54
+ delete mutableIntl.Segmenter
55
+ expect('Segmenter' in Intl).toBe(false)
56
+ try {
57
+ return fn(await import('./string'))
58
+ } finally {
59
+ mutableIntl.Segmenter = original
60
+ }
61
+ }
62
+
63
+ it('iterateGraphemes falls back to code-point iteration', async () => {
64
+ await withoutSegmenter(({ iterateGraphemes }) => {
65
+ expect([...iterateGraphemes('ab😀')]).toEqual(['a', 'b', '😀'])
66
+ })
67
+ })
68
+
69
+ it('getFirstCharacter keeps a surrogate pair whole', async () => {
70
+ await withoutSegmenter(({ getFirstCharacter }) => {
71
+ expect(getFirstCharacter('😀 hello')).toBe('😀')
72
+ })
73
+ })
74
+
75
+ it('getFirstCharacter splits a ZWJ cluster to its first code point rather than throwing', async () => {
76
+ await withoutSegmenter(({ getFirstCharacter }) => {
77
+ expect(getFirstCharacter('👨‍👩‍👧 family')).toBe('👨')
78
+ })
79
+ })
80
+ })
package/src/lib/string.ts CHANGED
@@ -1,4 +1,38 @@
1
1
  let graphemeSegmenter: Intl.Segmenter | undefined
2
+ let checkedForSegmenter = false
3
+
4
+ /**
5
+ * Resolve a shared grapheme `Intl.Segmenter`, or `undefined` when the runtime has no
6
+ * `Intl.Segmenter`. `Intl.Segmenter` is unavailable on some older browsers (notably Firefox before
7
+ * 125), so feature-detect it once rather than constructing it eagerly, which would throw at module
8
+ * load and take down the whole app.
9
+ */
10
+ function getGraphemeSegmenter(): Intl.Segmenter | undefined {
11
+ if (!checkedForSegmenter) {
12
+ checkedForSegmenter = true
13
+ if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
14
+ graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
15
+ }
16
+ }
17
+ return graphemeSegmenter
18
+ }
19
+
20
+ /**
21
+ * Iterate over the grapheme clusters of a string — user-perceived characters such as emoji
22
+ * sequences, flags, and accented letters — instead of UTF-16 code units. Uses `Intl.Segmenter`
23
+ * where available and falls back to iterating by code point on browsers that lack it, where a
24
+ * multi-code-point cluster is yielded as its component code points rather than as one segment.
25
+ *
26
+ * @public
27
+ */
28
+ export function* iterateGraphemes(str: string): IterableIterator<string> {
29
+ const segmenter = getGraphemeSegmenter()
30
+ if (segmenter) {
31
+ for (const { segment } of segmenter.segment(str)) yield segment
32
+ } else {
33
+ yield* str
34
+ }
35
+ }
2
36
 
3
37
  /**
4
38
  * Get the first character of a string, treating a multi-code-unit character such as an emoji as a
@@ -17,8 +51,7 @@ let graphemeSegmenter: Intl.Segmenter | undefined
17
51
  */
18
52
  export function getFirstCharacter(str: string): string {
19
53
  if (!str) return ''
20
- graphemeSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' })
21
- for (const { segment } of graphemeSegmenter.segment(str)) {
54
+ for (const segment of iterateGraphemes(str)) {
22
55
  return segment
23
56
  }
24
57
  return ''