@openeditor/react-native-prose-editor 0.0.7 → 0.0.8

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 (68) hide show
  1. package/android/build.gradle +35 -0
  2. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
  4. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
  5. package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
  9. package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
  10. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
  12. package/dist/EditorToolbar.d.ts +1 -1
  13. package/dist/EditorToolbar.js +19 -0
  14. package/dist/NativeEditorBridge.d.ts +62 -35
  15. package/dist/NativeEditorBridge.js +122 -79
  16. package/dist/NativeProseViewer.d.ts +13 -34
  17. package/dist/NativeProseViewer.js +40 -465
  18. package/dist/NativeRichTextEditor.d.ts +10 -1
  19. package/dist/NativeRichTextEditor.js +72 -34
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -1
  22. package/dist/schemas.d.ts +11 -0
  23. package/dist/schemas.js +149 -0
  24. package/dist/useNativeEditor.d.ts +3 -3
  25. package/dist/useNativeEditor.js +3 -3
  26. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  27. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  28. package/ios/Generated_editor_core.swift +97 -2
  29. package/ios/NativeBlockEditorSurface.swift +1393 -0
  30. package/ios/NativeEditorExpoView.swift +3 -3
  31. package/ios/NativeEditorModule.swift +30 -45
  32. package/ios/NativeInputSupport.swift +396 -0
  33. package/ios/PositionBridge.swift +4 -4
  34. package/ios/RichTextEditorView.swift +333 -786
  35. package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
  36. package/package.json +1 -1
  37. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  38. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  39. package/rust/android/x86_64/libeditor_core.so +0 -0
  40. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
  41. package/android/src/androidTest/AndroidManifest.xml +0 -8
  42. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
  43. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
  44. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
  45. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
  46. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
  47. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
  48. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
  49. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
  50. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
  51. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
  52. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
  53. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
  54. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
  55. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
  56. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
  57. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
  58. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
  59. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
  60. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
  61. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
  62. package/ios/EditorLayoutManager.swift +0 -774
  63. package/ios/NativeProseViewerExpoView.swift +0 -276
  64. package/ios/RenderBridge.swift +0 -1708
  65. package/ios/Tests/NativePerformanceTests.swift +0 -687
  66. package/ios/Tests/PositionBridgeTests.swift +0 -706
  67. package/ios/Tests/RenderBridgeTests.swift +0 -2236
  68. package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
@@ -1,461 +0,0 @@
1
- package com.openeditor.editor
2
-
3
- import org.junit.Assert.assertEquals
4
- import org.junit.Assert.assertTrue
5
- import org.junit.Test
6
- import org.junit.runner.RunWith
7
- import org.robolectric.RobolectricTestRunner
8
- import org.robolectric.annotation.Config
9
-
10
- /**
11
- * Unit tests for [PositionBridge] — UTF-16 <-> Unicode scalar offset conversion
12
- * and grapheme cluster boundary snapping.
13
- *
14
- * These tests are pure JVM (no Android framework dependencies) and mirror
15
- * the iOS PositionBridgeTests.swift test suite.
16
- *
17
- * Test coverage:
18
- * - ASCII text (1 UTF-16 = 1 scalar)
19
- * - BMP characters (accented chars, CJK)
20
- * - Surrogate pairs (emoji above U+FFFF)
21
- * - ZWJ sequences (family emoji)
22
- * - Regional indicator pairs (flag emoji)
23
- * - Mixed content
24
- * - Empty string edge cases
25
- * - Grapheme boundary snapping
26
- * - Roundtrip consistency
27
- */
28
- @RunWith(RobolectricTestRunner::class)
29
- @Config(sdk = [34])
30
- class PositionBridgeTest {
31
-
32
- // ── UTF-16 -> Scalar: ASCII ─────────────────────────────────────────
33
-
34
- /** ASCII characters are 1 UTF-16 code unit = 1 scalar each. */
35
- @Test
36
- fun `utf16ToScalar - ASCII only`() {
37
- val text = "Hello"
38
- // "Hello" = 5 UTF-16 code units = 5 scalars
39
- assertEquals(
40
- "Offset 0 should map to scalar 0",
41
- 0, PositionBridge.utf16ToScalar(0, text)
42
- )
43
- assertEquals(
44
- "Offset 1 in ASCII should map to scalar 1",
45
- 1, PositionBridge.utf16ToScalar(1, text)
46
- )
47
- assertEquals(
48
- "Offset 5 (end of 'Hello') should map to scalar 5",
49
- 5, PositionBridge.utf16ToScalar(5, text)
50
- )
51
- }
52
-
53
- /** Empty string edge case. */
54
- @Test
55
- fun `utf16ToScalar - empty string`() {
56
- assertEquals(
57
- "Empty string at offset 0 should return scalar 0",
58
- 0, PositionBridge.utf16ToScalar(0, "")
59
- )
60
- }
61
-
62
- // ── UTF-16 -> Scalar: BMP Characters ────────────────────────────────
63
-
64
- /** BMP characters (e.g. U+00E9, e-acute) are 1 UTF-16 code unit, 1 scalar. */
65
- @Test
66
- fun `utf16ToScalar - BMP characters`() {
67
- val text = "caf\u00E9" // "cafe" with accent
68
- assertEquals(
69
- "BMP character: UTF-16 offset 4 should be scalar 4",
70
- 4, PositionBridge.utf16ToScalar(4, text)
71
- )
72
- }
73
-
74
- // ── UTF-16 -> Scalar: Surrogate Pairs ───────────────────────────────
75
-
76
- /**
77
- * Characters above U+FFFF (supplementary plane) are encoded as
78
- * surrogate pairs in UTF-16: 2 code units per scalar.
79
- */
80
- @Test
81
- fun `utf16ToScalar - surrogate pair`() {
82
- // U+1F600 (grinning face) = 2 UTF-16 code units, 1 scalar
83
- val text = "A\uD83D\uDE00B" // "A" + U+1F600 + "B"
84
- // UTF-16: A(1) + U+1F600(2) + B(1) = 4 code units
85
- // Scalars: A(1) + U+1F600(1) + B(1) = 3 scalars
86
-
87
- assertEquals("Before 'A'", 0, PositionBridge.utf16ToScalar(0, text))
88
- assertEquals("After 'A', before emoji", 1, PositionBridge.utf16ToScalar(1, text))
89
- assertEquals(
90
- "After emoji (UTF-16 offset 3 = scalar 2)",
91
- 2, PositionBridge.utf16ToScalar(3, text)
92
- )
93
- assertEquals(
94
- "After 'B' (end of string)",
95
- 3, PositionBridge.utf16ToScalar(4, text)
96
- )
97
- }
98
-
99
- /**
100
- * The family emoji is composed of multiple scalars joined by ZWJ (U+200D).
101
- * U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
102
- * = 7 scalars, 11 UTF-16 code units (4 surrogates + 3 ZWJ)
103
- * But it renders as 1 grapheme cluster.
104
- */
105
- @Test
106
- fun `utf16ToScalar - family emoji (ZWJ sequence)`() {
107
- val text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
108
- val expectedUtf16Count = 11
109
- val expectedScalarCount = 7
110
-
111
- assertEquals(
112
- "Family emoji should be $expectedUtf16Count UTF-16 code units",
113
- expectedUtf16Count, text.length
114
- )
115
- assertEquals(
116
- "Family emoji should be $expectedScalarCount Unicode scalars",
117
- expectedScalarCount, text.codePointCount(0, text.length)
118
- )
119
- assertEquals(
120
- "Full family emoji: UTF-16 end should map to scalar end",
121
- expectedScalarCount,
122
- PositionBridge.utf16ToScalar(expectedUtf16Count, text)
123
- )
124
- }
125
-
126
- // ── UTF-16 -> Scalar: CJK ──────────────────────────────────────────
127
-
128
- /** CJK characters in the BMP: 1 UTF-16 = 1 scalar. */
129
- @Test
130
- fun `utf16ToScalar - CJK`() {
131
- val text = "\u4F60\u597D" // nihao
132
- assertEquals(
133
- "CJK character at offset 1 should be scalar 1",
134
- 1, PositionBridge.utf16ToScalar(1, text)
135
- )
136
- assertEquals(
137
- "End of CJK string should be scalar 2",
138
- 2, PositionBridge.utf16ToScalar(2, text)
139
- )
140
- }
141
-
142
- /** CJK extension B characters (U+20000+) are in the supplementary plane. */
143
- @Test
144
- fun `utf16ToScalar - CJK Extension B`() {
145
- // U+20000 (CJK Unified Ideographs Extension B) = 2 UTF-16 code units, 1 scalar
146
- val text = "A\uD840\uDC00B" // A + U+20000 + B
147
- // UTF-16: A(1) + U+20000(2) + B(1) = 4
148
- // Scalars: A(1) + U+20000(1) + B(1) = 3
149
- assertEquals(
150
- "After CJK Extension B character",
151
- 2, PositionBridge.utf16ToScalar(3, text)
152
- )
153
- }
154
-
155
- // ── UTF-16 -> Scalar: Mixed Content ─────────────────────────────────
156
-
157
- /** Mixed ASCII, emoji, and CJK in one string. */
158
- @Test
159
- fun `utf16ToScalar - mixed content`() {
160
- val text = "Hi\uD83D\uDE00\u4F60\u597D!"
161
- // UTF-16: H(1) + i(1) + U+1F600(2) + U+4F60(1) + U+597D(1) + !(1) = 7
162
- // Scalars: H(1) + i(1) + U+1F600(1) + U+4F60(1) + U+597D(1) + !(1) = 6
163
-
164
- assertEquals("After 'Hi'", 2, PositionBridge.utf16ToScalar(2, text))
165
- assertEquals(
166
- "After emoji (UTF-16 offset 4 = scalar 3)",
167
- 3, PositionBridge.utf16ToScalar(4, text)
168
- )
169
- assertEquals("End of mixed string", 6, PositionBridge.utf16ToScalar(7, text))
170
- }
171
-
172
- // ── UTF-16 -> Scalar: Flag Emoji ────────────────────────────────────
173
-
174
- /**
175
- * Flag emoji are composed of two regional indicator symbols.
176
- * e.g. US flag = U+1F1FA U+1F1F8 = 4 UTF-16 code units, 2 scalars, 1 grapheme.
177
- */
178
- @Test
179
- fun `utf16ToScalar - flag emoji`() {
180
- val text = "A\uD83C\uDDFA\uD83C\uDDF8B" // A + U+1F1FA + U+1F1F8 + B
181
- // UTF-16: A(1) + U+1F1FA(2) + U+1F1F8(2) + B(1) = 6
182
- // Scalars: A(1) + U+1F1FA(1) + U+1F1F8(1) + B(1) = 4
183
-
184
- assertEquals("After 'A'", 1, PositionBridge.utf16ToScalar(1, text))
185
- assertEquals(
186
- "After flag emoji (UTF-16 offset 5 = scalar 3)",
187
- 3, PositionBridge.utf16ToScalar(5, text)
188
- )
189
- assertEquals("After 'B'", 4, PositionBridge.utf16ToScalar(6, text))
190
- }
191
-
192
- // ── Scalar -> UTF-16: ASCII ─────────────────────────────────────────
193
-
194
- @Test
195
- fun `scalarToUtf16 - ASCII only`() {
196
- val text = "Hello"
197
- assertEquals(
198
- "Scalar 0 should map to UTF-16 offset 0",
199
- 0, PositionBridge.scalarToUtf16(0, text)
200
- )
201
- assertEquals(
202
- "Scalar 3 in ASCII should map to UTF-16 offset 3",
203
- 3, PositionBridge.scalarToUtf16(3, text)
204
- )
205
- assertEquals(
206
- "Scalar 5 (end) should map to UTF-16 offset 5",
207
- 5, PositionBridge.scalarToUtf16(5, text)
208
- )
209
- }
210
-
211
- // ── Scalar -> UTF-16: Surrogate Pairs ───────────────────────────────
212
-
213
- @Test
214
- fun `scalarToUtf16 - surrogate pair`() {
215
- val text = "A\uD83D\uDE00B" // A + U+1F600 + B
216
- // Scalar 0 = A -> UTF-16 offset 0
217
- // Scalar 1 = U+1F600 -> UTF-16 offset 1
218
- // Scalar 2 = B -> UTF-16 offset 3 (after 2 UTF-16 code units for emoji)
219
- assertEquals(
220
- "Scalar 1 (emoji start) should be UTF-16 offset 1",
221
- 1, PositionBridge.scalarToUtf16(1, text)
222
- )
223
- assertEquals(
224
- "Scalar 2 (after emoji) should be UTF-16 offset 3",
225
- 3, PositionBridge.scalarToUtf16(2, text)
226
- )
227
- assertEquals(
228
- "Scalar 3 (after B) should be UTF-16 offset 4",
229
- 4, PositionBridge.scalarToUtf16(3, text)
230
- )
231
- }
232
-
233
- // ── Scalar -> UTF-16: Family Emoji ──────────────────────────────────
234
-
235
- @Test
236
- fun `scalarToUtf16 - family emoji`() {
237
- val text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
238
- // 7 scalars, 11 UTF-16 code units
239
- assertEquals(
240
- "End of family emoji: scalar 7 -> UTF-16 offset 11",
241
- 11, PositionBridge.scalarToUtf16(7, text)
242
- )
243
- }
244
-
245
- // ── Roundtrip Consistency ───────────────────────────────────────────
246
-
247
- /** Verify that scalar->utf16->scalar roundtrips exactly. */
248
- @Test
249
- fun `roundtrip - scalar to utf16 and back`() {
250
- val testCases = listOf(
251
- "Hello, world!" to "ASCII",
252
- "A\uD83D\uDE00B" to "surrogate pair",
253
- "\u4F60\u597D" to "CJK",
254
- "abc\uD83D\uDE00\u4F60xyz" to "mixed"
255
- )
256
-
257
- for ((text, label) in testCases) {
258
- val scalarCount = text.codePointCount(0, text.length)
259
- for (scalarOffset in 0..scalarCount) {
260
- val utf16 = PositionBridge.scalarToUtf16(scalarOffset, text)
261
- val backToScalar = PositionBridge.utf16ToScalar(utf16, text)
262
- assertEquals(
263
- "Roundtrip for '$label' at scalar $scalarOffset: " +
264
- "utf16=$utf16, back=$backToScalar - should equal original scalar",
265
- scalarOffset, backToScalar
266
- )
267
- }
268
- }
269
- }
270
-
271
- /**
272
- * Verify that utf16->scalar->utf16 roundtrips correctly for various inputs.
273
- * Note: mid-surrogate offsets may snap forward, so we check >= originalOffset - 1.
274
- */
275
- @Test
276
- fun `roundtrip - utf16 to scalar and back`() {
277
- val testCases = listOf(
278
- "Hello, world!" to "ASCII",
279
- "A\uD83D\uDE00B" to "surrogate pair",
280
- "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66" to "family emoji",
281
- "\u4F60\u597D\u4E16\u754C" to "CJK",
282
- "abc\uD83D\uDE00\u4F60xyz" to "mixed",
283
- "A\u0301" to "combining character (A + combining acute)"
284
- )
285
-
286
- for ((text, label) in testCases) {
287
- for (utf16Offset in 0..text.length) {
288
- val scalar = PositionBridge.utf16ToScalar(utf16Offset, text)
289
- val backToUtf16 = PositionBridge.scalarToUtf16(scalar, text)
290
- assertTrue(
291
- "Roundtrip for '$label' at UTF-16 offset $utf16Offset: " +
292
- "scalar=$scalar, back=$backToUtf16",
293
- backToUtf16 >= utf16Offset - 1
294
- )
295
- }
296
- }
297
- }
298
-
299
- // ── Grapheme Boundary Snapping ──────────────────────────────────────
300
-
301
- /** Snapping at an already-valid boundary returns the same offset. */
302
- @Test
303
- fun `snapToGraphemeBoundary - already on boundary (ASCII)`() {
304
- val text = "Hello"
305
- for (i in 0..5) {
306
- assertEquals(
307
- "ASCII offset $i is already on a grapheme boundary",
308
- i, PositionBridge.snapToGraphemeBoundary(i, text)
309
- )
310
- }
311
- }
312
-
313
- /** Snapping in the middle of a surrogate pair snaps to the end of the grapheme cluster. */
314
- @Test
315
- fun `snapToGraphemeBoundary - mid surrogate pair`() {
316
- val text = "A\uD83D\uDE00B"
317
- // UTF-16: A(0), high surrogate(1), low surrogate(2), B(3)
318
- // Offset 2 is mid-surrogate. Should snap to 3 (end of emoji grapheme).
319
- val snapped = PositionBridge.snapToGraphemeBoundary(2, text)
320
- assertEquals(
321
- "Mid-surrogate offset 2 should snap to 3 (end of emoji grapheme)",
322
- 3, snapped
323
- )
324
- }
325
-
326
- /** Snapping in the middle of a family emoji snaps to the end. */
327
- @Test
328
- fun `snapToGraphemeBoundary - mid family emoji`() {
329
- val text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
330
- // The entire sequence is 1 grapheme cluster = 11 UTF-16 code units.
331
- // Any offset 1..10 should snap to 11.
332
- for (midOffset in 1..10) {
333
- val snapped = PositionBridge.snapToGraphemeBoundary(midOffset, text)
334
- assertEquals(
335
- "Mid-family-emoji offset $midOffset should snap to 11 (end of grapheme)",
336
- 11, snapped
337
- )
338
- }
339
- }
340
-
341
- /** Snapping in the middle of a combining character sequence. */
342
- @Test
343
- fun `snapToGraphemeBoundary - combining character`() {
344
- // "e" + combining acute accent = 2 UTF-16 code units but 1 grapheme cluster
345
- val text = "e\u0301"
346
- val snapped = PositionBridge.snapToGraphemeBoundary(1, text)
347
- assertEquals(
348
- "Between base and combining character should snap to end of grapheme",
349
- 2, snapped
350
- )
351
- }
352
-
353
- /** Snapping on empty string always returns 0. */
354
- @Test
355
- fun `snapToGraphemeBoundary - empty string`() {
356
- assertEquals(
357
- "Empty string should always return 0",
358
- 0, PositionBridge.snapToGraphemeBoundary(0, "")
359
- )
360
- assertEquals(
361
- "Empty string with out-of-range offset should return 0",
362
- 0, PositionBridge.snapToGraphemeBoundary(5, "")
363
- )
364
- }
365
-
366
- /** Snapping at the end of string returns the end offset. */
367
- @Test
368
- fun `snapToGraphemeBoundary - at end`() {
369
- val text = "Hello"
370
- assertEquals(
371
- "At end of string should return end offset",
372
- 5, PositionBridge.snapToGraphemeBoundary(5, text)
373
- )
374
- }
375
-
376
- /** Snapping with negative offset clamps to 0. */
377
- @Test
378
- fun `snapToGraphemeBoundary - negative offset`() {
379
- val text = "Hello"
380
- assertEquals(
381
- "Negative offset should clamp to 0",
382
- 0, PositionBridge.snapToGraphemeBoundary(-1, text)
383
- )
384
- }
385
-
386
- /** Flag emoji mid-offset snapping. */
387
- @Test
388
- fun `snapToGraphemeBoundary - flag emoji`() {
389
- val text = "\uD83C\uDDFA\uD83C\uDDF8" // US flag
390
- // 4 UTF-16 code units, 1 grapheme cluster.
391
- // Offsets 1, 2, 3 should snap to 4.
392
- for (midOffset in 1..3) {
393
- val snapped = PositionBridge.snapToGraphemeBoundary(midOffset, text)
394
- assertEquals(
395
- "Mid-flag-emoji offset $midOffset should snap to 4",
396
- 4, snapped
397
- )
398
- }
399
- }
400
-
401
- /** Scalar snapping only repairs surrogate-pair splits. */
402
- @Test
403
- fun `snapToScalarBoundary - mid surrogate pair`() {
404
- val text = "A\uD83D\uDE00B"
405
-
406
- assertEquals(1, PositionBridge.snapToScalarBoundary(2, text, biasForward = false))
407
- assertEquals(3, PositionBridge.snapToScalarBoundary(2, text, biasForward = true))
408
- }
409
-
410
- /** Ranges expand away from invalid scalar boundaries. */
411
- @Test
412
- fun `snapRangeToScalarBoundaries - expands split surrogate range`() {
413
- val text = "\uD83D\uDE00 ok"
414
-
415
- assertEquals(0 to 2, PositionBridge.snapRangeToScalarBoundaries(1, 1, text))
416
- }
417
-
418
- // ── Edge Cases ──────────────────────────────────────────────────────
419
-
420
- /** Offset beyond string length should be clamped. */
421
- @Test
422
- fun `utf16ToScalar - offset beyond string length`() {
423
- val text = "Hi"
424
- assertEquals(
425
- "Offset beyond length should count all scalars",
426
- 2, PositionBridge.utf16ToScalar(10, text)
427
- )
428
- }
429
-
430
- /** Scalar beyond code point count should return full UTF-16 length. */
431
- @Test
432
- fun `scalarToUtf16 - scalar beyond code point count`() {
433
- val text = "Hi"
434
- assertEquals(
435
- "Scalar beyond count should return full UTF-16 length",
436
- 2, PositionBridge.scalarToUtf16(10, text)
437
- )
438
- }
439
-
440
- /** Mid-surrogate UTF-16 offset should still produce a valid scalar count. */
441
- @Test
442
- fun `utf16ToScalar - mid surrogate pair offset`() {
443
- val text = "A\uD83D\uDE00B"
444
- // Offset 2 is in the middle of the surrogate pair for U+1F600.
445
- // The method walks code points, so at offset 2 we've consumed
446
- // A (1 code unit) + U+1F600 (2 code units, but offset 2 is mid-pair).
447
- // Since we count code points via Character.codePointAt which handles
448
- // surrogates, offset 2 still "fits" within the second code point's range,
449
- // so only 1 scalar (A) is fully before offset 2.
450
- val scalar = PositionBridge.utf16ToScalar(2, text)
451
- // Should be 1 (only 'A' fully precedes offset 2) or 2 (if the emoji
452
- // is counted because we started reading it). Implementation walks
453
- // with codePointAt, charCount — at utf16Pos=1 we read codePoint
454
- // U+1F600 (charCount=2), advance to utf16Pos=3 which > endIndex=2,
455
- // but we already incremented scalarCount. So result is 2.
456
- assertEquals(
457
- "Mid-surrogate offset 2: implementation counts the emoji scalar",
458
- 2, scalar
459
- )
460
- }
461
- }