@openeditor/react-native-prose-editor 0.0.6 → 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,774 +0,0 @@
1
- import UIKit
2
- import CoreText
3
-
4
- /// Draws list markers visually in the gutter without inserting them into the
5
- /// editable text storage. This keeps UIKit paragraph-start behaviors, such as
6
- /// sentence auto-capitalization, working naturally inside list items.
7
- final class EditorLayoutManager: NSLayoutManager {
8
- private(set) var blockquoteStripeDrawPassesForTesting: [[CGRect]] = []
9
-
10
- func blockquoteStripeRectsForTesting(
11
- in textStorage: NSTextStorage,
12
- visibleGlyphRange: NSRange? = nil,
13
- origin: CGPoint = .zero
14
- ) -> [CGRect] {
15
- let glyphsToShow = visibleGlyphRange ?? NSRange(location: 0, length: numberOfGlyphs)
16
- guard glyphsToShow.length > 0 else { return [] }
17
-
18
- let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
19
- let nsString = textStorage.string as NSString
20
- var drawnBlockquoteStarts = Set<Int>()
21
- var rects: [CGRect] = []
22
-
23
- textStorage.enumerateAttribute(
24
- RenderBridgeAttributes.blockquoteBorderColor,
25
- in: characterRange,
26
- options: [.longestEffectiveRangeNotRequired]
27
- ) { value, range, _ in
28
- guard range.length > 0, let color = value as? UIColor else { return }
29
-
30
- let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
31
- let paragraphStart = paragraphRange.location
32
- let groupRange = Self.blockquoteGroupCharacterRange(
33
- containing: paragraphStart,
34
- in: textStorage,
35
- nsString: nsString
36
- )
37
- let groupStart = groupRange.location
38
- guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
39
- guard let rect = blockquoteStripeRect(
40
- characterRange: groupRange,
41
- color: color,
42
- textStorage: textStorage,
43
- origin: origin
44
- ) else {
45
- return
46
- }
47
- rects.append(rect)
48
- }
49
-
50
- return rects
51
- }
52
-
53
- func resetBlockquoteStripeDrawPassesForTesting() {
54
- blockquoteStripeDrawPassesForTesting.removeAll()
55
- }
56
-
57
- override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
58
- guard let textStorage, glyphsToShow.length > 0 else { return }
59
-
60
- drawCodeBlockBackgrounds(
61
- in: textStorage,
62
- glyphsToShow: glyphsToShow,
63
- origin: origin
64
- )
65
- super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
66
-
67
- let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
68
- let nsString = textStorage.string as NSString
69
- var drawnParagraphStarts = Set<Int>()
70
- var drawnBlockquoteStarts = Set<Int>()
71
- var drawnStripeRects: [CGRect] = []
72
-
73
- textStorage.enumerateAttribute(
74
- RenderBridgeAttributes.listMarkerContext,
75
- in: characterRange,
76
- options: [.longestEffectiveRangeNotRequired]
77
- ) { value, range, _ in
78
- guard range.length > 0, let listContext = value as? [String: Any] else { return }
79
-
80
- let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
81
- let paragraphStart = paragraphRange.location
82
- guard !Self.isParagraphStartCreatedByHardBreak(paragraphStart, in: textStorage) else {
83
- return
84
- }
85
- guard drawnParagraphStarts.insert(paragraphStart).inserted else { return }
86
-
87
- self.drawListMarker(
88
- listContext: listContext,
89
- paragraphStart: paragraphStart,
90
- origin: origin,
91
- textStorage: textStorage
92
- )
93
- }
94
-
95
- textStorage.enumerateAttribute(
96
- RenderBridgeAttributes.blockquoteBorderColor,
97
- in: characterRange,
98
- options: [.longestEffectiveRangeNotRequired]
99
- ) { value, range, _ in
100
- guard range.length > 0, let color = value as? UIColor else { return }
101
-
102
- let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
103
- let paragraphStart = paragraphRange.location
104
- let groupRange = Self.blockquoteGroupCharacterRange(
105
- containing: paragraphStart,
106
- in: textStorage,
107
- nsString: nsString
108
- )
109
- let groupStart = groupRange.location
110
- guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
111
-
112
- guard let stripeRect = self.blockquoteStripeRect(
113
- characterRange: groupRange,
114
- color: color,
115
- textStorage: textStorage,
116
- origin: origin
117
- ) else {
118
- return
119
- }
120
- self.drawBlockquoteBorder(
121
- stripeRect: stripeRect,
122
- color: color
123
- )
124
- drawnStripeRects.append(stripeRect)
125
- }
126
-
127
- if !drawnStripeRects.isEmpty {
128
- blockquoteStripeDrawPassesForTesting.append(drawnStripeRects)
129
- }
130
- }
131
-
132
- func taskListMarkerParagraphStart(
133
- at point: CGPoint,
134
- in textStorage: NSTextStorage,
135
- textContainerOrigin: CGPoint
136
- ) -> Int? {
137
- guard numberOfGlyphs > 0 else { return nil }
138
-
139
- let nsString = textStorage.string as NSString
140
- let fullRange = NSRange(location: 0, length: textStorage.length)
141
- var seenStarts = Set<Int>()
142
- var matchedParagraphStart: Int?
143
-
144
- textStorage.enumerateAttribute(
145
- RenderBridgeAttributes.listMarkerContext,
146
- in: fullRange,
147
- options: [.longestEffectiveRangeNotRequired]
148
- ) { value, range, stop in
149
- guard range.length > 0,
150
- let listContext = value as? [String: Any],
151
- (listContext["kind"] as? String) == "task"
152
- else {
153
- return
154
- }
155
-
156
- let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
157
- let paragraphStart = paragraphRange.location
158
- guard !Self.isParagraphStartCreatedByHardBreak(paragraphStart, in: textStorage) else {
159
- return
160
- }
161
- guard seenStarts.insert(paragraphStart).inserted else { return }
162
- guard paragraphStart < textStorage.length else { return }
163
-
164
- let glyphIndex = self.glyphIndexForCharacter(at: paragraphStart)
165
- guard glyphIndex < self.numberOfGlyphs else { return }
166
-
167
- let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
168
- let baseFont = Self.markerBaseFont(from: attrs)
169
- let markerWidth = (attrs[RenderBridgeAttributes.listMarkerWidth] as? NSNumber)
170
- .map { CGFloat(truncating: $0) }
171
- ?? LayoutConstants.listMarkerWidth
172
-
173
- var lineGlyphRange = NSRange()
174
- let usedRect = self.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &lineGlyphRange)
175
- let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
176
- let glyphLocation = self.location(forGlyphAt: glyphIndex)
177
- let baselineY = lineFragmentRect.minY + glyphLocation.y
178
- let markerRect = Self.taskMarkerDrawingRect(
179
- usedRect: usedRect,
180
- lineFragmentRect: lineFragmentRect,
181
- markerWidth: markerWidth,
182
- baselineY: baselineY,
183
- baseFont: baseFont,
184
- origin: textContainerOrigin
185
- ).insetBy(dx: -10, dy: -8)
186
-
187
- if markerRect.contains(point) {
188
- matchedParagraphStart = paragraphStart
189
- stop.pointee = true
190
- }
191
- }
192
-
193
- return matchedParagraphStart
194
- }
195
-
196
- private func drawCodeBlockBackgrounds(
197
- in textStorage: NSTextStorage,
198
- glyphsToShow: NSRange,
199
- origin: CGPoint
200
- ) {
201
- let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
202
- let nsString = textStorage.string as NSString
203
- var drawnParagraphStarts = Set<Int>()
204
-
205
- textStorage.enumerateAttribute(
206
- RenderBridgeAttributes.codeBlockBackgroundColor,
207
- in: characterRange,
208
- options: [.longestEffectiveRangeNotRequired]
209
- ) { value, range, _ in
210
- guard range.length > 0, let color = value as? UIColor else { return }
211
-
212
- let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
213
- let paragraphStart = paragraphRange.location
214
- guard drawnParagraphStarts.insert(paragraphStart).inserted else { return }
215
-
216
- let codeBlockRange = Self.codeBlockCharacterRange(
217
- containing: paragraphStart,
218
- in: textStorage,
219
- nsString: nsString
220
- )
221
- guard let rect = self.codeBlockRect(
222
- characterRange: codeBlockRange,
223
- textStorage: textStorage,
224
- origin: origin
225
- ) else {
226
- return
227
- }
228
-
229
- let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
230
- let radius = (attrs[RenderBridgeAttributes.codeBlockBorderRadius] as? NSNumber)
231
- .map { CGFloat(truncating: $0) }
232
- ?? 8
233
-
234
- color.setFill()
235
- UIBezierPath(roundedRect: rect, cornerRadius: radius).fill()
236
- }
237
- }
238
-
239
- private func drawListMarker(
240
- listContext: [String: Any],
241
- paragraphStart: Int,
242
- origin: CGPoint,
243
- textStorage: NSTextStorage
244
- ) {
245
- guard paragraphStart < textStorage.length else { return }
246
-
247
- let glyphIndex = glyphIndexForCharacter(at: paragraphStart)
248
- guard glyphIndex < numberOfGlyphs else { return }
249
-
250
- var lineGlyphRange = NSRange()
251
- let usedRect = lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &lineGlyphRange)
252
- let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
253
- let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
254
-
255
- let baseFont = Self.markerBaseFont(from: attrs)
256
- let textColor = attrs[RenderBridgeAttributes.listMarkerColor] as? UIColor
257
- ?? attrs[.foregroundColor] as? UIColor
258
- ?? .label
259
- let markerScale = (attrs[RenderBridgeAttributes.listMarkerScale] as? NSNumber)
260
- .map { CGFloat(truncating: $0) }
261
- ?? LayoutConstants.unorderedListMarkerFontScale
262
- let markerWidth = (attrs[RenderBridgeAttributes.listMarkerWidth] as? NSNumber)
263
- .map { CGFloat(truncating: $0) }
264
- ?? LayoutConstants.listMarkerWidth
265
- let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
266
- let isTask = (listContext["kind"] as? String) == "task"
267
-
268
- let glyphLocation = location(forGlyphAt: glyphIndex)
269
- let baselineY = lineFragmentRect.minY + glyphLocation.y
270
-
271
- if isTask {
272
- let checkboxRect = Self.taskMarkerDrawingRect(
273
- usedRect: usedRect,
274
- lineFragmentRect: lineFragmentRect,
275
- markerWidth: markerWidth,
276
- baselineY: baselineY,
277
- baseFont: baseFont,
278
- origin: origin
279
- )
280
- drawTaskCheckbox(
281
- in: checkboxRect,
282
- checked: (listContext["checked"] as? NSNumber)?.boolValue ?? false,
283
- color: textColor
284
- )
285
- return
286
- }
287
-
288
- if ordered {
289
- let markerFont = markerFont(
290
- for: listContext,
291
- baseFont: baseFont,
292
- markerScale: markerScale
293
- )
294
- let markerText = RenderBridge.listMarkerString(listContext: listContext)
295
- let markerOrigin = Self.orderedMarkerDrawingOrigin(
296
- usedRect: usedRect,
297
- lineFragmentRect: lineFragmentRect,
298
- markerWidth: markerWidth,
299
- baselineY: baselineY,
300
- markerFont: markerFont,
301
- markerText: markerText,
302
- origin: origin
303
- )
304
- let markerAttrs: [NSAttributedString.Key: Any] = [
305
- .font: markerFont,
306
- .foregroundColor: textColor,
307
- ]
308
- NSAttributedString(string: markerText, attributes: markerAttrs).draw(at: markerOrigin)
309
- return
310
- }
311
-
312
- let bulletRect = Self.unorderedBulletDrawingRect(
313
- usedRect: usedRect,
314
- lineFragmentRect: lineFragmentRect,
315
- markerWidth: markerWidth,
316
- baselineY: baselineY,
317
- baseFont: baseFont,
318
- markerScale: markerScale,
319
- origin: origin
320
- )
321
- let path = UIBezierPath(ovalIn: bulletRect)
322
- textColor.setFill()
323
- path.fill()
324
- }
325
-
326
- private func blockquoteStripeRect(
327
- characterRange: NSRange,
328
- color: UIColor,
329
- textStorage: NSTextStorage,
330
- origin: CGPoint
331
- ) -> CGRect? {
332
- guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
333
- return nil
334
- }
335
-
336
- ensureLayout(forCharacterRange: characterRange)
337
- let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
338
- guard glyphRange.length > 0 else { return nil }
339
-
340
- var topEdge: CGFloat?
341
- var bottomEdge: CGFloat?
342
- var textLeadingEdge: CGFloat?
343
- enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
344
- let verticalReferenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
345
- if let currentTop = topEdge {
346
- topEdge = min(currentTop, lineFragmentRect.minY)
347
- } else {
348
- topEdge = lineFragmentRect.minY
349
- }
350
- if let currentBottom = bottomEdge {
351
- bottomEdge = max(currentBottom, verticalReferenceRect.maxY)
352
- } else {
353
- bottomEdge = verticalReferenceRect.maxY
354
- }
355
- let referenceMinX = usedRect.width > 0 ? usedRect.minX : lineFragmentRect.minX
356
- if let current = textLeadingEdge {
357
- textLeadingEdge = min(current, referenceMinX)
358
- } else {
359
- textLeadingEdge = referenceMinX
360
- }
361
- }
362
- guard let topEdge, let bottomEdge, bottomEdge > topEdge, let textLeadingEdge else { return nil }
363
-
364
- let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
365
- let borderWidth = (attrs[RenderBridgeAttributes.blockquoteBorderWidth] as? NSNumber)
366
- .map { CGFloat(truncating: $0) }
367
- ?? LayoutConstants.blockquoteBorderWidth
368
- let gap = (attrs[RenderBridgeAttributes.blockquoteMarkerGap] as? NSNumber)
369
- .map { CGFloat(truncating: $0) }
370
- ?? LayoutConstants.blockquoteMarkerGap
371
-
372
- let stripeX = origin.x + textLeadingEdge - gap - borderWidth
373
- let stripeRect = CGRect(
374
- x: stripeX,
375
- y: origin.y + topEdge,
376
- width: borderWidth,
377
- height: bottomEdge - topEdge
378
- )
379
- return stripeRect
380
- }
381
-
382
- private func drawBlockquoteBorder(
383
- stripeRect: CGRect,
384
- color: UIColor
385
- ) {
386
- color.setFill()
387
- UIBezierPath(rect: stripeRect).fill()
388
- }
389
-
390
- private static func blockquoteGroupCharacterRange(
391
- containing paragraphStart: Int,
392
- in textStorage: NSTextStorage,
393
- nsString: NSString
394
- ) -> NSRange {
395
- let initialParagraphRange = nsString.paragraphRange(
396
- for: NSRange(location: paragraphStart, length: 0)
397
- )
398
- var groupStart = initialParagraphRange.location
399
- var groupEnd = NSMaxRange(initialParagraphRange)
400
-
401
- var probeStart = groupStart
402
- while probeStart > 0 {
403
- let previousParagraphRange = nsString.paragraphRange(
404
- for: NSRange(location: probeStart - 1, length: 0)
405
- )
406
- let previousStart = previousParagraphRange.location
407
- guard paragraphHasBlockquoteBorder(
408
- previousParagraphRange,
409
- in: textStorage
410
- )
411
- else {
412
- break
413
- }
414
-
415
- groupStart = previousStart
416
- probeStart = previousStart
417
- }
418
-
419
- var nextParagraphLocation = groupEnd
420
- while nextParagraphLocation < textStorage.length {
421
- let nextParagraphRange = nsString.paragraphRange(
422
- for: NSRange(location: nextParagraphLocation, length: 0)
423
- )
424
- guard paragraphHasBlockquoteBorder(
425
- nextParagraphRange,
426
- in: textStorage
427
- )
428
- else {
429
- break
430
- }
431
-
432
- groupEnd = NSMaxRange(nextParagraphRange)
433
- nextParagraphLocation = groupEnd
434
- }
435
-
436
- return NSRange(location: groupStart, length: groupEnd - groupStart)
437
- }
438
-
439
- private static func paragraphHasBlockquoteBorder(
440
- _ paragraphRange: NSRange,
441
- in textStorage: NSTextStorage
442
- ) -> Bool {
443
- guard paragraphRange.length > 0 else { return false }
444
- let nsString = textStorage.string as NSString
445
- var sawQuotedContent = false
446
- var sawAnyQuotedCharacter = false
447
-
448
- for offset in 0..<paragraphRange.length {
449
- let index = paragraphRange.location + offset
450
- guard index < textStorage.length else { break }
451
-
452
- let hasBorder = textStorage.attribute(
453
- RenderBridgeAttributes.blockquoteBorderColor,
454
- at: index,
455
- effectiveRange: nil
456
- ) != nil
457
- guard hasBorder else { continue }
458
- sawAnyQuotedCharacter = true
459
-
460
- let scalar = nsString.character(at: index)
461
- if scalar != 0x000A, scalar != 0x000D {
462
- sawQuotedContent = true
463
- break
464
- }
465
- }
466
-
467
- if sawQuotedContent {
468
- return true
469
- }
470
-
471
- let trimmed = nsString.substring(with: paragraphRange)
472
- .trimmingCharacters(in: .newlines)
473
- return trimmed.isEmpty && sawAnyQuotedCharacter
474
- }
475
-
476
- private static func codeBlockCharacterRange(
477
- containing paragraphStart: Int,
478
- in textStorage: NSTextStorage,
479
- nsString: NSString
480
- ) -> NSRange {
481
- let initialParagraphRange = nsString.paragraphRange(
482
- for: NSRange(location: paragraphStart, length: 0)
483
- )
484
- var groupStart = initialParagraphRange.location
485
- var groupEnd = NSMaxRange(initialParagraphRange)
486
-
487
- var probeStart = groupStart
488
- while probeStart > 0 {
489
- let previousParagraphRange = nsString.paragraphRange(
490
- for: NSRange(location: probeStart - 1, length: 0)
491
- )
492
- guard paragraphHasCodeBlockBackground(previousParagraphRange, in: textStorage) else {
493
- break
494
- }
495
- groupStart = previousParagraphRange.location
496
- probeStart = previousParagraphRange.location
497
- }
498
-
499
- var nextParagraphLocation = groupEnd
500
- while nextParagraphLocation < textStorage.length {
501
- let nextParagraphRange = nsString.paragraphRange(
502
- for: NSRange(location: nextParagraphLocation, length: 0)
503
- )
504
- guard paragraphHasCodeBlockBackground(nextParagraphRange, in: textStorage) else {
505
- break
506
- }
507
- groupEnd = NSMaxRange(nextParagraphRange)
508
- nextParagraphLocation = groupEnd
509
- }
510
-
511
- return NSRange(location: groupStart, length: groupEnd - groupStart)
512
- }
513
-
514
- private static func paragraphHasCodeBlockBackground(
515
- _ paragraphRange: NSRange,
516
- in textStorage: NSTextStorage
517
- ) -> Bool {
518
- guard paragraphRange.length > 0 else { return false }
519
- return textStorage.attribute(
520
- RenderBridgeAttributes.codeBlockBackgroundColor,
521
- at: paragraphRange.location,
522
- effectiveRange: nil
523
- ) != nil
524
- }
525
-
526
- private func codeBlockRect(
527
- characterRange: NSRange,
528
- textStorage: NSTextStorage,
529
- origin: CGPoint
530
- ) -> CGRect? {
531
- guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
532
- return nil
533
- }
534
-
535
- ensureLayout(forCharacterRange: characterRange)
536
- let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
537
- guard glyphRange.length > 0 else { return nil }
538
-
539
- let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
540
- let horizontalPadding = (attrs[RenderBridgeAttributes.codeBlockPaddingHorizontal] as? NSNumber)
541
- .map { CGFloat(truncating: $0) }
542
- ?? 12
543
- let verticalPadding = (attrs[RenderBridgeAttributes.codeBlockPaddingVertical] as? NSNumber)
544
- .map { CGFloat(truncating: $0) }
545
- ?? 8
546
-
547
- var minX: CGFloat?
548
- var maxX: CGFloat?
549
- var minY: CGFloat?
550
- var maxY: CGFloat?
551
-
552
- enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
553
- let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
554
- let lineMinX = referenceRect.minX - horizontalPadding
555
- let lineMaxX = lineFragmentRect.maxX + horizontalPadding
556
- let lineMinY = lineFragmentRect.minY
557
- let lineMaxY = referenceRect.maxY
558
-
559
- minX = min(minX ?? lineMinX, lineMinX)
560
- maxX = max(maxX ?? lineMaxX, lineMaxX)
561
- minY = min(minY ?? lineMinY, lineMinY)
562
- maxY = max(maxY ?? lineMaxY, lineMaxY)
563
- }
564
-
565
- guard let minX, let maxX, let minY, let maxY, maxY > minY else { return nil }
566
-
567
- return CGRect(
568
- x: origin.x + minX,
569
- y: origin.y + minY - verticalPadding,
570
- width: maxX - minX,
571
- height: (maxY - minY) + (verticalPadding * 2)
572
- )
573
- }
574
-
575
- static func markerParagraphStyle(from attrs: [NSAttributedString.Key: Any]) -> NSMutableParagraphStyle {
576
- let markerStyle = NSMutableParagraphStyle()
577
- let sourceStyle = attrs[.paragraphStyle] as? NSParagraphStyle
578
-
579
- markerStyle.minimumLineHeight = sourceStyle?.minimumLineHeight ?? 0
580
- markerStyle.maximumLineHeight = sourceStyle?.maximumLineHeight ?? 0
581
- markerStyle.lineHeightMultiple = sourceStyle?.lineHeightMultiple ?? 0
582
- markerStyle.baseWritingDirection = sourceStyle?.baseWritingDirection ?? .natural
583
- markerStyle.alignment = .right
584
- markerStyle.lineBreakMode = .byClipping
585
- markerStyle.firstLineHeadIndent = 0
586
- markerStyle.headIndent = 0
587
- markerStyle.tailIndent = 0
588
-
589
- return markerStyle
590
- }
591
-
592
- static func markerDrawingRect(
593
- usedRect: CGRect,
594
- lineFragmentRect: CGRect,
595
- markerWidth: CGFloat,
596
- baselineY: CGFloat,
597
- markerFont: UIFont,
598
- origin: CGPoint
599
- ) -> CGRect {
600
- let typographicHeight = markerFont.ascender - markerFont.descender
601
- let leading = max(markerFont.lineHeight - typographicHeight, 0)
602
- let topY = baselineY - markerFont.ascender - (leading / 2.0)
603
- let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
604
- return CGRect(
605
- x: origin.x + referenceRect.minX - markerWidth,
606
- y: origin.y + topY,
607
- width: markerWidth - 4.0,
608
- height: markerFont.lineHeight
609
- )
610
- }
611
-
612
- static func taskMarkerDrawingRect(
613
- usedRect: CGRect,
614
- lineFragmentRect: CGRect,
615
- markerWidth: CGFloat,
616
- baselineY: CGFloat,
617
- baseFont: UIFont,
618
- origin: CGPoint
619
- ) -> CGRect {
620
- let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
621
- let checkboxSize = min(
622
- max(baseFont.lineHeight * 1.05, 24),
623
- max(markerWidth - 4, 24)
624
- )
625
- let centerY = baselineY - ((baseFont.ascender + baseFont.descender) / 2.0)
626
- let x = origin.x + referenceRect.minX - LayoutConstants.listMarkerTextGap - checkboxSize
627
- let y = origin.y + centerY - (checkboxSize / 2.0)
628
- return CGRect(x: x, y: y, width: checkboxSize, height: checkboxSize)
629
- }
630
-
631
- static func orderedMarkerDrawingOrigin(
632
- usedRect: CGRect,
633
- lineFragmentRect: CGRect,
634
- markerWidth: CGFloat,
635
- baselineY: CGFloat,
636
- markerFont: UIFont,
637
- markerText: String,
638
- origin: CGPoint
639
- ) -> CGPoint {
640
- let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
641
- let visibleMarkerText = markerText.trimmingCharacters(in: .whitespaces)
642
- let markerSize = (visibleMarkerText as NSString).size(withAttributes: [
643
- .font: markerFont,
644
- ])
645
- let rightInset: CGFloat = 4.0
646
- let x = origin.x + referenceRect.minX - rightInset - ceil(markerSize.width)
647
- let y = origin.y + baselineY - markerFont.ascender
648
- return CGPoint(x: x, y: y)
649
- }
650
-
651
- static func markerBaselineOffset(
652
- for listContext: [String: Any],
653
- baseFont: UIFont,
654
- markerFont: UIFont
655
- ) -> CGFloat {
656
- let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
657
- guard !ordered else { return 0 }
658
-
659
- let targetMidline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
660
- let glyphMidline = unorderedBulletGlyphMidline(for: markerFont)
661
- return targetMidline - glyphMidline
662
- }
663
-
664
- static func unorderedBulletDrawingRect(
665
- usedRect: CGRect,
666
- lineFragmentRect: CGRect,
667
- markerWidth: CGFloat,
668
- baselineY: CGFloat,
669
- baseFont: UIFont,
670
- markerScale: CGFloat,
671
- origin: CGPoint
672
- ) -> CGRect {
673
- let markerFont = baseFont.withSize(baseFont.pointSize * markerScale)
674
- let bulletBounds = unorderedBulletGlyphBounds(for: markerFont)
675
- let bulletDiameter = max(max(bulletBounds.width, bulletBounds.height), 1)
676
- let targetCenterAboveBaseline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
677
- let centerY = baselineY - targetCenterAboveBaseline
678
- let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
679
- let rightInset = LayoutConstants.listMarkerTextGap
680
- let x = origin.x + referenceRect.minX - rightInset - bulletDiameter
681
- let y = origin.y + centerY - (bulletDiameter / 2.0)
682
-
683
- return CGRect(
684
- x: x,
685
- y: y,
686
- width: bulletDiameter,
687
- height: bulletDiameter
688
- )
689
- }
690
-
691
- static func isParagraphStartCreatedByHardBreak(
692
- _ paragraphStart: Int,
693
- in textStorage: NSTextStorage
694
- ) -> Bool {
695
- guard paragraphStart > 0, paragraphStart <= textStorage.length else { return false }
696
- let previousVoidType = textStorage.attribute(
697
- RenderBridgeAttributes.voidNodeType,
698
- at: paragraphStart - 1,
699
- effectiveRange: nil
700
- ) as? String
701
- return previousVoidType == "hardBreak"
702
- }
703
-
704
- private func markerFont(
705
- for listContext: [String: Any],
706
- baseFont: UIFont,
707
- markerScale: CGFloat
708
- ) -> UIFont {
709
- let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
710
- let isTask = (listContext["kind"] as? String) == "task"
711
- if ordered {
712
- return baseFont
713
- }
714
- if isTask {
715
- return baseFont.withSize(baseFont.pointSize * 1.35)
716
- }
717
- return baseFont.withSize(baseFont.pointSize * markerScale)
718
- }
719
-
720
- private func drawTaskCheckbox(
721
- in rect: CGRect,
722
- checked: Bool,
723
- color: UIColor
724
- ) {
725
- let path = UIBezierPath(roundedRect: rect, cornerRadius: min(rect.width, rect.height) * 0.22)
726
- color.setStroke()
727
- path.lineWidth = max(1.8, rect.width * 0.09)
728
- path.stroke()
729
-
730
- guard checked else { return }
731
-
732
- let checkPath = UIBezierPath()
733
- checkPath.move(to: CGPoint(x: rect.minX + rect.width * 0.22, y: rect.midY + rect.height * 0.04))
734
- checkPath.addLine(to: CGPoint(x: rect.minX + rect.width * 0.42, y: rect.maxY - rect.height * 0.24))
735
- checkPath.addLine(to: CGPoint(x: rect.maxX - rect.width * 0.18, y: rect.minY + rect.height * 0.24))
736
- checkPath.lineCapStyle = .round
737
- checkPath.lineJoinStyle = .round
738
- checkPath.lineWidth = max(2.1, rect.width * 0.12)
739
- color.setStroke()
740
- checkPath.stroke()
741
- }
742
-
743
- static func markerBaseFont(
744
- from attrs: [NSAttributedString.Key: Any],
745
- fallback fallbackFont: UIFont = .systemFont(ofSize: 16)
746
- ) -> UIFont {
747
- (attrs[RenderBridgeAttributes.listMarkerBaseFont] as? UIFont)
748
- ?? (attrs[.font] as? UIFont)
749
- ?? fallbackFont
750
- }
751
-
752
- private static func unorderedBulletGlyphBounds(for font: UIFont) -> CGRect {
753
- let ctFont = font as CTFont
754
- let bullet = UniChar(0x2022)
755
- var glyph = CGGlyph()
756
- guard CTFontGetGlyphsForCharacters(ctFont, [bullet], &glyph, 1) else {
757
- let fallbackDiameter = max(font.pointSize * 0.28, 1)
758
- return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
759
- }
760
-
761
- var boundingRect = CGRect.zero
762
- CTFontGetBoundingRectsForGlyphs(ctFont, .default, [glyph], &boundingRect, 1)
763
- if boundingRect.isNull || boundingRect.isEmpty {
764
- let fallbackDiameter = max(font.pointSize * 0.28, 1)
765
- return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
766
- }
767
-
768
- return boundingRect
769
- }
770
-
771
- private static func unorderedBulletGlyphMidline(for font: UIFont) -> CGFloat {
772
- unorderedBulletGlyphBounds(for: font).midY
773
- }
774
- }