@openeditor/react-native-prose-editor 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.md +180 -0
- package/android/build.gradle +69 -0
- package/android/consumer-rules.pro +18 -0
- package/android/src/androidTest/AndroidManifest.xml +8 -0
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
- package/app.plugin.js +62 -0
- package/dist/EditorTheme.d.ts +113 -0
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +191 -0
- package/dist/EditorToolbar.js +1095 -0
- package/dist/NativeEditorBridge.d.ts +325 -0
- package/dist/NativeEditorBridge.js +933 -0
- package/dist/NativeProseViewer.d.ts +53 -0
- package/dist/NativeProseViewer.js +480 -0
- package/dist/NativeRichTextEditor.d.ts +183 -0
- package/dist/NativeRichTextEditor.js +2574 -0
- package/dist/YjsCollaboration.d.ts +90 -0
- package/dist/YjsCollaboration.js +743 -0
- package/dist/addons.d.ts +100 -0
- package/dist/addons.js +82 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +263 -0
- package/dist/useNativeEditor.d.ts +42 -0
- package/dist/useNativeEditor.js +124 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +327 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +774 -0
- package/ios/EditorTheme.swift +515 -0
- package/ios/Generated_editor_core.swift +1513 -0
- package/ios/NativeEditorExpoView.swift +3593 -0
- package/ios/NativeEditorModule.swift +624 -0
- package/ios/NativeProseViewerExpoView.swift +276 -0
- package/ios/PositionBridge.swift +558 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +1708 -0
- package/ios/RichTextEditorView.swift +6150 -0
- package/ios/Tests/NativePerformanceTests.swift +687 -0
- package/ios/Tests/PositionBridgeTests.swift +706 -0
- package/ios/Tests/RenderBridgeTests.swift +2236 -0
- package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +74 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
|
@@ -0,0 +1,774 @@
|
|
|
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
|
+
}
|