@streamplace/components 0.0.1 → 0.7.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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,32 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { usePDSAgent } from "../streamplace-store/xrpc";
3
+ export function useAvatars(dids) {
4
+ let agent = usePDSAgent();
5
+ const [profiles, setProfiles] = useState({});
6
+ const inFlight = useRef(new Set());
7
+ const missingDids = useMemo(() => dids.filter((did) => !(did in profiles) && !inFlight.current.has(did)), [dids, profiles]);
8
+ useEffect(() => {
9
+ if (missingDids.length === 0 || !agent)
10
+ return;
11
+ const toFetch = missingDids.slice(0, 25);
12
+ toFetch.forEach((did) => inFlight.current.add(did));
13
+ const fetchProfiles = async () => {
14
+ try {
15
+ const result = await agent.getProfiles({ actors: toFetch });
16
+ const newProfiles = {};
17
+ result.data.profiles.forEach((p) => {
18
+ newProfiles[p.did] = p;
19
+ });
20
+ setProfiles((prev) => ({ ...prev, ...newProfiles }));
21
+ }
22
+ catch (e) {
23
+ console.error("Failed to fetch profiles", e);
24
+ }
25
+ finally {
26
+ toFetch.forEach((did) => inFlight.current.delete(did));
27
+ }
28
+ };
29
+ fetchProfiles();
30
+ }, [missingDids, agent]);
31
+ return profiles;
32
+ }
@@ -0,0 +1,9 @@
1
+ import { usePlayerStore } from "../player-store";
2
+ export function useCameraToggle() {
3
+ const ingestCamera = usePlayerStore((x) => x.ingestCamera);
4
+ const setIngestCamera = usePlayerStore((x) => x.setIngestCamera);
5
+ const doSetIngestCamera = () => {
6
+ setIngestCamera(ingestCamera === "user" ? "environment" : "user");
7
+ };
8
+ return { ingestCamera, doSetIngestCamera };
9
+ }
@@ -0,0 +1,33 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Keyboard } from "react-native";
3
+ export function useKeyboard() {
4
+ const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
5
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
6
+ useEffect(() => {
7
+ const willShowSubscription = Keyboard.addListener("keyboardWillShow", (e) => {
8
+ // setIsKeyboardVisible(true);
9
+ setKeyboardHeight(e.endCoordinates.height);
10
+ console.log("keyboardWillShow", e.endCoordinates.height);
11
+ });
12
+ const willHideSubscription = Keyboard.addListener("keyboardWillHide", (e) => {
13
+ // setIsKeyboardVisible(false);
14
+ setKeyboardHeight(0);
15
+ console.log("keyboardWillHide", e.endCoordinates.height);
16
+ });
17
+ const showSubscription = Keyboard.addListener("keyboardDidShow", (e) => {
18
+ setIsKeyboardVisible(true);
19
+ setKeyboardHeight(e.endCoordinates.height);
20
+ console.log("keyboardDidShow", e.endCoordinates.height);
21
+ });
22
+ const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
23
+ setIsKeyboardVisible(false);
24
+ setKeyboardHeight(0);
25
+ });
26
+ return () => {
27
+ showSubscription.remove();
28
+ hideSubscription.remove();
29
+ willShowSubscription.remove();
30
+ };
31
+ }, []);
32
+ return { isKeyboardVisible, keyboardHeight };
33
+ }
@@ -0,0 +1,11 @@
1
+ import { useKeyboard } from "../hooks/useKeyboard";
2
+ import { useOuterAndInnerDimensions } from "../hooks/useOuterAndInnerDimensions";
3
+ export function useKeyboardSlide() {
4
+ const { keyboardHeight } = useKeyboard();
5
+ const { outerHeight, innerHeight } = useOuterAndInnerDimensions();
6
+ let slideKeyboard = 0;
7
+ if (keyboardHeight > 0) {
8
+ slideKeyboard = -keyboardHeight + (outerHeight - innerHeight);
9
+ }
10
+ return { keyboardHeight, slideKeyboard };
11
+ }
@@ -0,0 +1,62 @@
1
+ import { useState } from "react";
2
+ import { useLivestreamStore } from "../livestream-store";
3
+ import { usePlayerStore } from "../player-store";
4
+ import { useCreateStreamRecord } from "../streamplace-store";
5
+ export function useLivestreamInfo() {
6
+ const ingest = usePlayerStore((x) => x.ingestConnectionState);
7
+ const profile = useLivestreamStore((x) => x.profile);
8
+ const ingestStarting = usePlayerStore((x) => x.ingestStarting);
9
+ const setIngestStarting = usePlayerStore((x) => x.setIngestStarting);
10
+ const setIngestLive = usePlayerStore((x) => x.setIngestLive);
11
+ const createStreamRecord = useCreateStreamRecord();
12
+ const [title, setTitle] = useState("");
13
+ const [showCountdown, setShowCountdown] = useState(false);
14
+ const [recordSubmitted, setRecordSubmitted] = useState(false);
15
+ const handleSubmit = async () => {
16
+ try {
17
+ if (title !== "") {
18
+ setRecordSubmitted(true);
19
+ await createStreamRecord(title);
20
+ }
21
+ }
22
+ catch (error) {
23
+ console.error("Error creating livestream:", error);
24
+ throw new Error("Failed to create livestream record");
25
+ }
26
+ finally {
27
+ setRecordSubmitted(false);
28
+ }
29
+ };
30
+ const toggleGoLive = (keyboardHeight, closeKeyboard) => {
31
+ if (!ingestStarting) {
32
+ // Optionally close keyboard if provided
33
+ if (closeKeyboard)
34
+ closeKeyboard();
35
+ setShowCountdown(true);
36
+ setIngestStarting(true);
37
+ setIngestLive(true);
38
+ // wait ~3 seconds before announcing
39
+ setTimeout(() => {
40
+ handleSubmit();
41
+ }, 3000);
42
+ }
43
+ else {
44
+ setIngestStarting(false);
45
+ setIngestLive(false);
46
+ }
47
+ };
48
+ return {
49
+ ingest,
50
+ profile,
51
+ title,
52
+ setTitle,
53
+ showCountdown,
54
+ setShowCountdown,
55
+ recordSubmitted,
56
+ setRecordSubmitted,
57
+ ingestStarting,
58
+ setIngestStarting,
59
+ handleSubmit,
60
+ toggleGoLive,
61
+ };
62
+ }
@@ -0,0 +1,27 @@
1
+ import { useCallback, useState } from "react";
2
+ export function useOuterAndInnerDimensions() {
3
+ const [outerDimensions, setOuterDimensions] = useState({
4
+ width: 0,
5
+ height: 0,
6
+ });
7
+ const [innerDimensions, setInnerDimensions] = useState({
8
+ width: 0,
9
+ height: 0,
10
+ });
11
+ const onOuterLayout = useCallback((event) => {
12
+ const { width, height } = event.nativeEvent.layout;
13
+ setOuterDimensions({ width, height });
14
+ }, []);
15
+ const onInnerLayout = useCallback((event) => {
16
+ const { width, height } = event.nativeEvent.layout;
17
+ setInnerDimensions({ width, height });
18
+ }, []);
19
+ return {
20
+ outerWidth: outerDimensions.width,
21
+ outerHeight: outerDimensions.height,
22
+ innerWidth: innerDimensions.width,
23
+ innerHeight: innerDimensions.height,
24
+ onOuterLayout,
25
+ onInnerLayout,
26
+ };
27
+ }
@@ -0,0 +1,19 @@
1
+ import { Dimensions } from "react-native";
2
+ import { usePlayerStore } from "../player-store";
3
+ /**
4
+ * usePlayerDimensions
5
+ * Returns player and device dimensions, and whether the player aspect ratio is greater than the device's.
6
+ */
7
+ export function usePlayerDimensions() {
8
+ const { width, height } = Dimensions.get("window");
9
+ const pHeight = Number(usePlayerStore((x) => x.playerHeight)) || 0;
10
+ const pWidth = Number(usePlayerStore((x) => x.playerWidth)) || 0;
11
+ const isPlayerRatioGreater = pHeight > 0 && pWidth > 0 ? pWidth / pHeight > width / height : false;
12
+ return {
13
+ width,
14
+ height,
15
+ pWidth,
16
+ pHeight,
17
+ isPlayerRatioGreater,
18
+ };
19
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useLivestreamStore } from "../livestream-store";
3
+ function getLiveConnectionQuality(timeBetweenSegments, range, numOfSegments = 1) {
4
+ if (timeBetweenSegments === null || range === null)
5
+ return "poor";
6
+ if (timeBetweenSegments <= 1500 && range <= (1500 * 60) / numOfSegments) {
7
+ return "good";
8
+ }
9
+ if (timeBetweenSegments <= 3000 && range <= (3000 * 60) / numOfSegments) {
10
+ return "degraded";
11
+ }
12
+ return "poor";
13
+ }
14
+ export function useSegmentTiming() {
15
+ const latestSegment = useLivestreamStore((x) => x.segment);
16
+ const [segmentDeltas, setSegmentDeltas] = useState([]);
17
+ const prevSegmentRef = useRef();
18
+ const prevTimestampRef = useRef(null);
19
+ // Dummy state to force update every second
20
+ const [, setNow] = useState(Date.now());
21
+ useEffect(() => {
22
+ const interval = setInterval(() => {
23
+ setNow(Date.now());
24
+ }, 1000);
25
+ return () => clearInterval(interval);
26
+ }, []);
27
+ useEffect(() => {
28
+ if (latestSegment && prevSegmentRef.current !== latestSegment) {
29
+ const now = Date.now();
30
+ if (prevTimestampRef.current !== null) {
31
+ const delta = now - prevTimestampRef.current;
32
+ // Only store the last 25 deltas
33
+ setSegmentDeltas((prev) => [...prev, delta].slice(-25));
34
+ }
35
+ prevTimestampRef.current = now;
36
+ prevSegmentRef.current = latestSegment;
37
+ }
38
+ }, [latestSegment]);
39
+ // The most recent time between segments
40
+ const timeBetweenSegments = segmentDeltas.length > 0
41
+ ? segmentDeltas[segmentDeltas.length - 1]
42
+ : prevTimestampRef.current
43
+ ? Date.now() - prevTimestampRef.current
44
+ : null;
45
+ // Calculate mean and range of deltas
46
+ const mean = segmentDeltas.length > 0
47
+ ? Math.round(segmentDeltas.reduce((acc, curr) => acc + curr, 0) /
48
+ segmentDeltas.length)
49
+ : null;
50
+ const range = segmentDeltas.length > 0
51
+ ? Math.max(...segmentDeltas) - Math.min(...segmentDeltas)
52
+ : null;
53
+ let to_ret = {
54
+ segmentDeltas,
55
+ timeBetweenSegments,
56
+ mean,
57
+ range,
58
+ connectionQuality: "poor",
59
+ };
60
+ to_ret.connectionQuality = getLiveConnectionQuality(timeBetweenSegments, range, segmentDeltas.length);
61
+ return to_ret;
62
+ }
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // barrel file :)
2
+ export * from "./livestream-provider";
3
+ export * from "./livestream-store";
4
+ export * from "./player-store";
5
+ export * from "./streamplace-provider";
6
+ export * from "./streamplace-store";
7
+ export { PlayerProvider, withPlayerProvider, } from "./player-store/player-provider";
8
+ export { usePlayerContext } from "./player-store/player-store";
9
+ export { Player, PlayerUI } from "./components/mobile-player/player";
10
+ export * as ui from "./components/ui";
11
+ export * from "./components/ui";
12
+ export * as theme from "./lib/theme";
13
+ export * as atoms from "./lib/theme/atoms";
14
+ export * from "./hooks";
15
+ export * from "./components/chat/chat";
16
+ export * from "./components/chat/chat-box";
@@ -0,0 +1,88 @@
1
+ const segment = (text, features) => {
2
+ return { text, features: text.length > 0 ? features : undefined };
3
+ };
4
+ export const segmentize = (text, facets) => {
5
+ if (facets === undefined || facets.length === 0) {
6
+ return [segment(text, undefined)];
7
+ }
8
+ const segments = [];
9
+ const utf16Length = text.length;
10
+ let utf16Cursor = 0;
11
+ let utf8Cursor = 0;
12
+ const advanceCursor = (startUtf16, endUtf8) => {
13
+ let curs = startUtf16;
14
+ // Fast-path for entirely ASCII text
15
+ const isLikelyAsciiText = text.charCodeAt(curs) < 0x80;
16
+ if (isLikelyAsciiText) {
17
+ curs += 1;
18
+ utf8Cursor += 1;
19
+ // SIMD-like batch processing
20
+ while (utf8Cursor + 8 <= endUtf8 && curs + 8 <= utf16Length) {
21
+ const char1 = text.charCodeAt(curs);
22
+ const char2 = text.charCodeAt(curs + 1);
23
+ const char3 = text.charCodeAt(curs + 2);
24
+ const char4 = text.charCodeAt(curs + 3);
25
+ const char5 = text.charCodeAt(curs + 4);
26
+ const char6 = text.charCodeAt(curs + 5);
27
+ const char7 = text.charCodeAt(curs + 6);
28
+ const char8 = text.charCodeAt(curs + 7);
29
+ if ((char1 | char2 | char3 | char4 | char5 | char6 | char7 | char8) <
30
+ 0x80) {
31
+ curs += 8;
32
+ utf8Cursor += 8;
33
+ continue;
34
+ }
35
+ break;
36
+ }
37
+ }
38
+ // Process remaining characters individually
39
+ while (utf8Cursor < endUtf8 && curs < utf16Length) {
40
+ const code = text.charCodeAt(curs);
41
+ if (code < 0x80) {
42
+ curs += 1;
43
+ utf8Cursor += 1;
44
+ }
45
+ else if (code < 0x800) {
46
+ curs += 1;
47
+ utf8Cursor += 2;
48
+ }
49
+ else if (code < 0xd800 || code > 0xdbff) {
50
+ curs += 1;
51
+ utf8Cursor += 3;
52
+ }
53
+ else {
54
+ curs += 2;
55
+ utf8Cursor += 4;
56
+ }
57
+ }
58
+ return curs;
59
+ };
60
+ // Process facets
61
+ for (let idx = 0, len = facets.length; idx < len; idx++) {
62
+ const facet = facets[idx];
63
+ const { byteStart, byteEnd } = facet.index;
64
+ const features = facet.features;
65
+ if (byteStart > byteEnd || features.length === 0) {
66
+ continue;
67
+ }
68
+ if (utf8Cursor < byteStart) {
69
+ const nextUtf16Cursor = advanceCursor(utf16Cursor, byteStart);
70
+ if (nextUtf16Cursor > utf16Cursor) {
71
+ segments.push(segment(text.slice(utf16Cursor, nextUtf16Cursor), undefined));
72
+ }
73
+ utf16Cursor = nextUtf16Cursor;
74
+ }
75
+ {
76
+ const nextUtf16Cursor = advanceCursor(utf16Cursor, byteEnd);
77
+ if (nextUtf16Cursor > utf16Cursor) {
78
+ segments.push(segment(text.slice(utf16Cursor, nextUtf16Cursor), features));
79
+ }
80
+ utf16Cursor = nextUtf16Cursor;
81
+ }
82
+ }
83
+ // Handle remaining text
84
+ if (utf16Cursor < utf16Length) {
85
+ segments.push(segment(text.slice(utf16Cursor), undefined));
86
+ }
87
+ return segments;
88
+ };