@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.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- 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/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- 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 +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,2236 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import CoreText
|
|
3
|
-
|
|
4
|
-
// MARK: - RenderBridge Tests
|
|
5
|
-
|
|
6
|
-
final class RenderBridgeTests: XCTestCase {
|
|
7
|
-
|
|
8
|
-
// MARK: - Test Fixtures
|
|
9
|
-
|
|
10
|
-
private let baseFont = UIFont.systemFont(ofSize: 16)
|
|
11
|
-
private let textColor = UIColor.black
|
|
12
|
-
|
|
13
|
-
func testViewerEmptyCollapseDetectsDocumentsWithOnlyEmptyTopLevelParagraphs() {
|
|
14
|
-
let json = """
|
|
15
|
-
[
|
|
16
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
17
|
-
{"type": "textRun", "text": "\\u200B", "marks": []},
|
|
18
|
-
{"type": "blockEnd"},
|
|
19
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
20
|
-
{"type": "textRun", "text": "", "marks": []},
|
|
21
|
-
{"type": "blockEnd"}
|
|
22
|
-
]
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
XCTAssertTrue(NativeProseViewerExpoView.renderJsonContainsOnlyEmptyParagraphs(json))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func testViewerEmptyCollapseKeepsVisibleRenderedContentMeasurable() {
|
|
29
|
-
let json = """
|
|
30
|
-
[
|
|
31
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
32
|
-
{"type": "textRun", "text": "Hello", "marks": []},
|
|
33
|
-
{"type": "blockEnd"}
|
|
34
|
-
]
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
XCTAssertFalse(NativeProseViewerExpoView.renderJsonContainsOnlyEmptyParagraphs(json))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func testViewerEmptyCollapseKeepsNonParagraphRenderedBlocksMeasurable() {
|
|
41
|
-
let json = """
|
|
42
|
-
[
|
|
43
|
-
{"type": "voidBlock", "nodeType": "image", "docPos": 1, "attrs": {}}
|
|
44
|
-
]
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
XCTAssertFalse(NativeProseViewerExpoView.renderJsonContainsOnlyEmptyParagraphs(json))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// MARK: - Plain Text Rendering
|
|
51
|
-
|
|
52
|
-
/// A single paragraph with unstyled text should produce the text with base font.
|
|
53
|
-
func testRender_plainParagraph() {
|
|
54
|
-
let json = """
|
|
55
|
-
[
|
|
56
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
57
|
-
{"type": "textRun", "text": "Hello, world!", "marks": []},
|
|
58
|
-
{"type": "blockEnd"}
|
|
59
|
-
]
|
|
60
|
-
"""
|
|
61
|
-
let result = RenderBridge.renderElements(
|
|
62
|
-
fromJSON: json,
|
|
63
|
-
baseFont: baseFont,
|
|
64
|
-
textColor: textColor
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
XCTAssertEqual(
|
|
68
|
-
result.string, "Hello, world!",
|
|
69
|
-
"Plain paragraph should render as the text content"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
// Verify the font is the base font.
|
|
73
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
74
|
-
let font = attrs[.font] as? UIFont
|
|
75
|
-
XCTAssertNotNil(font, "Should have a font attribute")
|
|
76
|
-
XCTAssertEqual(
|
|
77
|
-
font?.pointSize, baseFont.pointSize,
|
|
78
|
-
"Font size should match base font"
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
func testRender_leadingTopLevelChildMetadataCoversWholeEmoji() {
|
|
83
|
-
let blocks: [[[String: Any]]] = [[
|
|
84
|
-
["type": "blockStart", "nodeType": "paragraph", "depth": 0],
|
|
85
|
-
["type": "textRun", "text": "😀", "marks": []],
|
|
86
|
-
["type": "blockEnd"],
|
|
87
|
-
]]
|
|
88
|
-
|
|
89
|
-
let result = RenderBridge.renderBlocks(
|
|
90
|
-
fromArray: blocks,
|
|
91
|
-
baseFont: baseFont,
|
|
92
|
-
textColor: textColor
|
|
93
|
-
)
|
|
94
|
-
let firstComposedRange = (result.string as NSString)
|
|
95
|
-
.rangeOfComposedCharacterSequence(at: 0)
|
|
96
|
-
var effectiveRange = NSRange(location: NSNotFound, length: 0)
|
|
97
|
-
let value = result.attribute(
|
|
98
|
-
RenderBridgeAttributes.topLevelChildIndex,
|
|
99
|
-
at: 0,
|
|
100
|
-
longestEffectiveRange: &effectiveRange,
|
|
101
|
-
in: NSRange(location: 0, length: result.length)
|
|
102
|
-
) as? NSNumber
|
|
103
|
-
|
|
104
|
-
XCTAssertEqual(result.string, "😀")
|
|
105
|
-
XCTAssertGreaterThan(firstComposedRange.length, 1, "test must cover a surrogate-pair emoji")
|
|
106
|
-
XCTAssertEqual(value?.intValue, 0)
|
|
107
|
-
XCTAssertEqual(
|
|
108
|
-
effectiveRange,
|
|
109
|
-
firstComposedRange,
|
|
110
|
-
"top-level metadata must not split a leading emoji surrogate pair into separate attribute runs"
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// MARK: - Bold Text Rendering
|
|
115
|
-
|
|
116
|
-
/// Bold mark should produce a font with the bold trait.
|
|
117
|
-
func testRender_boldText() {
|
|
118
|
-
let json = """
|
|
119
|
-
[
|
|
120
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
121
|
-
{"type": "textRun", "text": "bold text", "marks": ["bold"]},
|
|
122
|
-
{"type": "blockEnd"}
|
|
123
|
-
]
|
|
124
|
-
"""
|
|
125
|
-
let result = RenderBridge.renderElements(
|
|
126
|
-
fromJSON: json,
|
|
127
|
-
baseFont: baseFont,
|
|
128
|
-
textColor: textColor
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
XCTAssertEqual(result.string, "bold text")
|
|
132
|
-
|
|
133
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
134
|
-
let font = attrs[.font] as? UIFont
|
|
135
|
-
XCTAssertNotNil(font, "Should have a font attribute")
|
|
136
|
-
XCTAssertTrue(
|
|
137
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
138
|
-
"Font should have bold trait. Got font: \(String(describing: font))"
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// MARK: - Italic Text Rendering
|
|
143
|
-
|
|
144
|
-
func testRender_italicText() {
|
|
145
|
-
let json = """
|
|
146
|
-
[
|
|
147
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
148
|
-
{"type": "textRun", "text": "italic text", "marks": ["italic"]},
|
|
149
|
-
{"type": "blockEnd"}
|
|
150
|
-
]
|
|
151
|
-
"""
|
|
152
|
-
let result = RenderBridge.renderElements(
|
|
153
|
-
fromJSON: json,
|
|
154
|
-
baseFont: baseFont,
|
|
155
|
-
textColor: textColor
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
XCTAssertEqual(result.string, "italic text")
|
|
159
|
-
|
|
160
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
161
|
-
let font = attrs[.font] as? UIFont
|
|
162
|
-
XCTAssertNotNil(font, "Should have a font attribute")
|
|
163
|
-
XCTAssertTrue(
|
|
164
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitItalic) ?? false,
|
|
165
|
-
"Font should have italic trait. Got font: \(String(describing: font))"
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// MARK: - Bold + Italic Combined
|
|
170
|
-
|
|
171
|
-
func testRender_boldItalic() {
|
|
172
|
-
let json = """
|
|
173
|
-
[
|
|
174
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
175
|
-
{"type": "textRun", "text": "bold italic", "marks": ["bold", "italic"]},
|
|
176
|
-
{"type": "blockEnd"}
|
|
177
|
-
]
|
|
178
|
-
"""
|
|
179
|
-
let result = RenderBridge.renderElements(
|
|
180
|
-
fromJSON: json,
|
|
181
|
-
baseFont: baseFont,
|
|
182
|
-
textColor: textColor
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
186
|
-
let font = attrs[.font] as? UIFont
|
|
187
|
-
XCTAssertNotNil(font, "Should have a font attribute")
|
|
188
|
-
|
|
189
|
-
let traits = font?.fontDescriptor.symbolicTraits ?? []
|
|
190
|
-
XCTAssertTrue(
|
|
191
|
-
traits.contains(.traitBold),
|
|
192
|
-
"Font should have bold trait. Traits: \(traits)"
|
|
193
|
-
)
|
|
194
|
-
XCTAssertTrue(
|
|
195
|
-
traits.contains(.traitItalic),
|
|
196
|
-
"Font should have italic trait. Traits: \(traits)"
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// MARK: - Underline
|
|
201
|
-
|
|
202
|
-
func testRender_underline() {
|
|
203
|
-
let json = """
|
|
204
|
-
[
|
|
205
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
206
|
-
{"type": "textRun", "text": "underlined", "marks": ["underline"]},
|
|
207
|
-
{"type": "blockEnd"}
|
|
208
|
-
]
|
|
209
|
-
"""
|
|
210
|
-
let result = RenderBridge.renderElements(
|
|
211
|
-
fromJSON: json,
|
|
212
|
-
baseFont: baseFont,
|
|
213
|
-
textColor: textColor
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
217
|
-
let underline = attrs[.underlineStyle] as? Int
|
|
218
|
-
XCTAssertNotNil(underline, "Should have underline style attribute")
|
|
219
|
-
XCTAssertEqual(
|
|
220
|
-
underline, NSUnderlineStyle.single.rawValue,
|
|
221
|
-
"Underline should be single. Got: \(String(describing: underline))"
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// MARK: - Strikethrough
|
|
226
|
-
|
|
227
|
-
func testRender_strikethrough() {
|
|
228
|
-
let json = """
|
|
229
|
-
[
|
|
230
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
231
|
-
{"type": "textRun", "text": "struck", "marks": ["strike"]},
|
|
232
|
-
{"type": "blockEnd"}
|
|
233
|
-
]
|
|
234
|
-
"""
|
|
235
|
-
let result = RenderBridge.renderElements(
|
|
236
|
-
fromJSON: json,
|
|
237
|
-
baseFont: baseFont,
|
|
238
|
-
textColor: textColor
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
242
|
-
let strikethrough = attrs[.strikethroughStyle] as? Int
|
|
243
|
-
XCTAssertNotNil(strikethrough, "Should have strikethrough style attribute")
|
|
244
|
-
XCTAssertEqual(
|
|
245
|
-
strikethrough, NSUnderlineStyle.single.rawValue,
|
|
246
|
-
"Strikethrough should be single. Got: \(String(describing: strikethrough))"
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
func testRender_linkMarkObjectAppliesVisualLinkStylingWithoutInteractiveAttribute() {
|
|
251
|
-
let json = """
|
|
252
|
-
[
|
|
253
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
254
|
-
{"type": "textRun", "text": "OpenAI", "marks": [{"type": "link", "href": "https://openai.com"}]},
|
|
255
|
-
{"type": "blockEnd"}
|
|
256
|
-
]
|
|
257
|
-
"""
|
|
258
|
-
let result = RenderBridge.renderElements(
|
|
259
|
-
fromJSON: json,
|
|
260
|
-
baseFont: baseFont,
|
|
261
|
-
textColor: textColor
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
265
|
-
XCTAssertEqual(
|
|
266
|
-
attrs[.underlineStyle] as? Int,
|
|
267
|
-
NSUnderlineStyle.single.rawValue
|
|
268
|
-
)
|
|
269
|
-
XCTAssertEqual(attrs[.foregroundColor] as? UIColor, UIColor.systemBlue)
|
|
270
|
-
XCTAssertNil(attrs[.link])
|
|
271
|
-
XCTAssertEqual(
|
|
272
|
-
attrs[RenderBridgeAttributes.linkHref] as? String,
|
|
273
|
-
"https://openai.com"
|
|
274
|
-
)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
func testRender_linkMarkUsesThemeOverrides() {
|
|
278
|
-
let json = """
|
|
279
|
-
[
|
|
280
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
281
|
-
{"type": "textRun", "text": "OpenAI", "marks": [{"type": "link", "href": "https://openai.com"}]},
|
|
282
|
-
{"type": "blockEnd"}
|
|
283
|
-
]
|
|
284
|
-
"""
|
|
285
|
-
let result = RenderBridge.renderElements(
|
|
286
|
-
fromJSON: json,
|
|
287
|
-
baseFont: baseFont,
|
|
288
|
-
textColor: textColor,
|
|
289
|
-
theme: EditorTheme(dictionary: [
|
|
290
|
-
"links": [
|
|
291
|
-
"color": "#445566",
|
|
292
|
-
"backgroundColor": "#eef6ff",
|
|
293
|
-
"fontSize": 18,
|
|
294
|
-
"fontWeight": "700",
|
|
295
|
-
"fontStyle": "italic",
|
|
296
|
-
"underline": false,
|
|
297
|
-
],
|
|
298
|
-
])
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
302
|
-
let font = attrs[.font] as? UIFont
|
|
303
|
-
XCTAssertEqual(attrs[.foregroundColor] as? UIColor, EditorTheme.color(from: "#445566"))
|
|
304
|
-
XCTAssertEqual(attrs[.backgroundColor] as? UIColor, EditorTheme.color(from: "#eef6ff"))
|
|
305
|
-
XCTAssertNil(attrs[.underlineStyle])
|
|
306
|
-
XCTAssertEqual(font?.pointSize, 18)
|
|
307
|
-
XCTAssertTrue(font?.fontDescriptor.symbolicTraits.contains(.traitBold) == true)
|
|
308
|
-
XCTAssertTrue(font?.fontDescriptor.symbolicTraits.contains(.traitItalic) == true)
|
|
309
|
-
XCTAssertEqual(
|
|
310
|
-
attrs[RenderBridgeAttributes.linkHref] as? String,
|
|
311
|
-
"https://openai.com"
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
func testRenderBlocks_withLeadingSeparatorDoesNotDuplicateTopLevelChildIndexOnContent() {
|
|
316
|
-
let blocks: [[[String: Any]]] = [[
|
|
317
|
-
["type": "blockStart", "nodeType": "paragraph", "depth": 0],
|
|
318
|
-
["type": "textRun", "text": "Hello", "marks": []],
|
|
319
|
-
["type": "blockEnd"],
|
|
320
|
-
]]
|
|
321
|
-
|
|
322
|
-
let result = RenderBridge.renderBlocks(
|
|
323
|
-
fromArray: blocks,
|
|
324
|
-
startIndex: 3,
|
|
325
|
-
includeLeadingInterBlockSeparator: true,
|
|
326
|
-
baseFont: baseFont,
|
|
327
|
-
textColor: textColor
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
XCTAssertEqual(result.string, "\nHello")
|
|
331
|
-
XCTAssertEqual(
|
|
332
|
-
(result.attribute(RenderBridgeAttributes.topLevelChildIndex, at: 0, effectiveRange: nil)
|
|
333
|
-
as? NSNumber)?.intValue,
|
|
334
|
-
3
|
|
335
|
-
)
|
|
336
|
-
XCTAssertNil(
|
|
337
|
-
result.attribute(RenderBridgeAttributes.topLevelChildIndex, at: 1, effectiveRange: nil),
|
|
338
|
-
"Leading content should not duplicate the separator's top-level child index"
|
|
339
|
-
)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// MARK: - Code Mark (Monospace)
|
|
343
|
-
|
|
344
|
-
func testRender_codeInline() {
|
|
345
|
-
let json = """
|
|
346
|
-
[
|
|
347
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
348
|
-
{"type": "textRun", "text": "code", "marks": ["code"]},
|
|
349
|
-
{"type": "blockEnd"}
|
|
350
|
-
]
|
|
351
|
-
"""
|
|
352
|
-
let result = RenderBridge.renderElements(
|
|
353
|
-
fromJSON: json,
|
|
354
|
-
baseFont: baseFont,
|
|
355
|
-
textColor: textColor
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
359
|
-
let font = attrs[.font] as? UIFont
|
|
360
|
-
XCTAssertNotNil(font, "Should have a font attribute")
|
|
361
|
-
XCTAssertTrue(
|
|
362
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) ?? false,
|
|
363
|
-
"Code mark should produce monospace font. Got font: \(String(describing: font))"
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// MARK: - Hard Break (Void Inline)
|
|
368
|
-
|
|
369
|
-
/// A hardBreak void inline should render as a newline character.
|
|
370
|
-
func testRender_hardBreak() {
|
|
371
|
-
let json = """
|
|
372
|
-
[
|
|
373
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
374
|
-
{"type": "textRun", "text": "Line 1", "marks": []},
|
|
375
|
-
{"type": "voidInline", "nodeType": "hardBreak", "docPos": 7},
|
|
376
|
-
{"type": "textRun", "text": "Line 2", "marks": []},
|
|
377
|
-
{"type": "blockEnd"}
|
|
378
|
-
]
|
|
379
|
-
"""
|
|
380
|
-
let result = RenderBridge.renderElements(
|
|
381
|
-
fromJSON: json,
|
|
382
|
-
baseFont: baseFont,
|
|
383
|
-
textColor: textColor
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
XCTAssertEqual(
|
|
387
|
-
result.string, "Line 1\nLine 2",
|
|
388
|
-
"Hard break should render as newline. Got: '\(result.string)'"
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
// Verify the newline character has the void attribute.
|
|
392
|
-
let newlineIndex = 6 // "Line 1" = 6 chars, newline at index 6
|
|
393
|
-
let attrs = result.attributes(at: newlineIndex, effectiveRange: nil)
|
|
394
|
-
let voidType = attrs[RenderBridgeAttributes.voidNodeType] as? String
|
|
395
|
-
XCTAssertEqual(
|
|
396
|
-
voidType, "hardBreak",
|
|
397
|
-
"Newline should have voidNodeType='hardBreak' attribute. Got: \(String(describing: voidType))"
|
|
398
|
-
)
|
|
399
|
-
let docPos = attrs[RenderBridgeAttributes.docPos] as? UInt32
|
|
400
|
-
XCTAssertEqual(
|
|
401
|
-
docPos, 7,
|
|
402
|
-
"Newline should have docPos=7. Got: \(String(describing: docPos))"
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
func testRender_hardBreakDoesNotKeepParagraphSpacingBetweenVisualLines() {
|
|
407
|
-
let json = """
|
|
408
|
-
[
|
|
409
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
410
|
-
{"type": "textRun", "text": "Line 1", "marks": []},
|
|
411
|
-
{"type": "voidInline", "nodeType": "hardBreak", "docPos": 7},
|
|
412
|
-
{"type": "textRun", "text": "Line 2", "marks": []},
|
|
413
|
-
{"type": "blockEnd"}
|
|
414
|
-
]
|
|
415
|
-
"""
|
|
416
|
-
let theme = EditorTheme(dictionary: [
|
|
417
|
-
"paragraph": [
|
|
418
|
-
"spacingAfter": 14,
|
|
419
|
-
],
|
|
420
|
-
])
|
|
421
|
-
let result = RenderBridge.renderElements(
|
|
422
|
-
fromJSON: json,
|
|
423
|
-
baseFont: baseFont,
|
|
424
|
-
textColor: textColor,
|
|
425
|
-
theme: theme
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
let leadingStyle = result.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
|
|
429
|
-
let newlineStyle = result.attribute(.paragraphStyle, at: 6, effectiveRange: nil) as? NSParagraphStyle
|
|
430
|
-
|
|
431
|
-
XCTAssertEqual(leadingStyle?.paragraphSpacing ?? -1, 0, accuracy: 0.1)
|
|
432
|
-
XCTAssertEqual(newlineStyle?.paragraphSpacing ?? -1, 0, accuracy: 0.1)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
func testRender_trailingHardBreakAppendsSyntheticPlaceholder() {
|
|
436
|
-
let json = """
|
|
437
|
-
[
|
|
438
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
439
|
-
{"type": "textRun", "text": "A", "marks": []},
|
|
440
|
-
{"type": "voidInline", "nodeType": "hardBreak", "docPos": 2},
|
|
441
|
-
{"type": "blockEnd"}
|
|
442
|
-
]
|
|
443
|
-
"""
|
|
444
|
-
let result = RenderBridge.renderElements(
|
|
445
|
-
fromJSON: json,
|
|
446
|
-
baseFont: baseFont,
|
|
447
|
-
textColor: textColor
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
XCTAssertEqual(result.string, "A\n\u{200B}")
|
|
451
|
-
let placeholderIndex = (result.string as NSString).length - 1
|
|
452
|
-
XCTAssertEqual(
|
|
453
|
-
result.attribute(RenderBridgeAttributes.syntheticPlaceholder, at: placeholderIndex, effectiveRange: nil) as? Bool,
|
|
454
|
-
true
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
func testRender_trailingHardBreakPlaceholderKeepsBlockquoteBorderAttributes() {
|
|
459
|
-
let json = """
|
|
460
|
-
[
|
|
461
|
-
{"type": "blockStart", "nodeType": "blockquote", "depth": 0},
|
|
462
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
463
|
-
{"type": "textRun", "text": "A", "marks": []},
|
|
464
|
-
{"type": "voidInline", "nodeType": "hardBreak", "docPos": 2},
|
|
465
|
-
{"type": "blockEnd"},
|
|
466
|
-
{"type": "blockEnd"}
|
|
467
|
-
]
|
|
468
|
-
"""
|
|
469
|
-
let result = RenderBridge.renderElements(
|
|
470
|
-
fromJSON: json,
|
|
471
|
-
baseFont: baseFont,
|
|
472
|
-
textColor: textColor
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
XCTAssertEqual(result.string, "A\n\u{200B}")
|
|
476
|
-
|
|
477
|
-
let placeholderIndex = (result.string as NSString).length - 1
|
|
478
|
-
XCTAssertEqual(
|
|
479
|
-
result.attribute(RenderBridgeAttributes.syntheticPlaceholder, at: placeholderIndex, effectiveRange: nil) as? Bool,
|
|
480
|
-
true
|
|
481
|
-
)
|
|
482
|
-
XCTAssertNotNil(
|
|
483
|
-
result.attribute(RenderBridgeAttributes.blockquoteBorderColor, at: placeholderIndex, effectiveRange: nil),
|
|
484
|
-
"trailing hard-break placeholder inside a blockquote should keep blockquote styling"
|
|
485
|
-
)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// MARK: - Horizontal Rule (Void Block)
|
|
489
|
-
|
|
490
|
-
/// A horizontalRule should render as U+FFFC with an NSTextAttachment.
|
|
491
|
-
func testRender_horizontalRule() {
|
|
492
|
-
let json = """
|
|
493
|
-
[
|
|
494
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
495
|
-
{"type": "textRun", "text": "Above", "marks": []},
|
|
496
|
-
{"type": "blockEnd"},
|
|
497
|
-
{"type": "voidBlock", "nodeType": "horizontalRule", "docPos": 7},
|
|
498
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
499
|
-
{"type": "textRun", "text": "Below", "marks": []},
|
|
500
|
-
{"type": "blockEnd"}
|
|
501
|
-
]
|
|
502
|
-
"""
|
|
503
|
-
let result = RenderBridge.renderElements(
|
|
504
|
-
fromJSON: json,
|
|
505
|
-
baseFont: baseFont,
|
|
506
|
-
textColor: textColor
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
// The expected structure is: "Above" + "\n" + U+FFFC + "\n" + "Below"
|
|
510
|
-
// The newlines are inter-block separators.
|
|
511
|
-
let string = result.string
|
|
512
|
-
XCTAssertTrue(
|
|
513
|
-
string.contains("\u{FFFC}"),
|
|
514
|
-
"Horizontal rule should contain object replacement character. Got: '\(string)'"
|
|
515
|
-
)
|
|
516
|
-
|
|
517
|
-
// Find the FFFC character and check its attributes.
|
|
518
|
-
if let fffcRange = string.range(of: "\u{FFFC}") {
|
|
519
|
-
let nsRange = NSRange(fffcRange, in: string)
|
|
520
|
-
let attrs = result.attributes(at: nsRange.location, effectiveRange: nil)
|
|
521
|
-
|
|
522
|
-
let voidType = attrs[RenderBridgeAttributes.voidNodeType] as? String
|
|
523
|
-
XCTAssertEqual(
|
|
524
|
-
voidType, "horizontalRule",
|
|
525
|
-
"FFFC should have voidNodeType='horizontalRule'. Got: \(String(describing: voidType))"
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
let attachment = attrs[.attachment] as? NSTextAttachment
|
|
529
|
-
XCTAssertNotNil(
|
|
530
|
-
attachment,
|
|
531
|
-
"FFFC should have an NSTextAttachment"
|
|
532
|
-
)
|
|
533
|
-
XCTAssertTrue(
|
|
534
|
-
attachment is HorizontalRuleAttachment,
|
|
535
|
-
"Attachment should be HorizontalRuleAttachment. Got: \(String(describing: type(of: attachment)))"
|
|
536
|
-
)
|
|
537
|
-
} else {
|
|
538
|
-
XCTFail("Could not find FFFC character in rendered string")
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
func testRender_horizontalRuleCollapsesAdjacentParagraphSpacing() {
|
|
543
|
-
let json = """
|
|
544
|
-
[
|
|
545
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
546
|
-
{"type": "textRun", "text": "Above", "marks": []},
|
|
547
|
-
{"type": "blockEnd"},
|
|
548
|
-
{"type": "voidBlock", "nodeType": "horizontalRule", "docPos": 7},
|
|
549
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
550
|
-
{"type": "textRun", "text": "Below", "marks": []},
|
|
551
|
-
{"type": "blockEnd"}
|
|
552
|
-
]
|
|
553
|
-
"""
|
|
554
|
-
let theme = EditorTheme(dictionary: [
|
|
555
|
-
"paragraph": [
|
|
556
|
-
"spacingAfter": 14,
|
|
557
|
-
],
|
|
558
|
-
"horizontalRule": [
|
|
559
|
-
"verticalMargin": 10,
|
|
560
|
-
],
|
|
561
|
-
])
|
|
562
|
-
|
|
563
|
-
let result = RenderBridge.renderElements(
|
|
564
|
-
fromJSON: json,
|
|
565
|
-
baseFont: baseFont,
|
|
566
|
-
textColor: textColor,
|
|
567
|
-
theme: theme
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
let nsString = result.string as NSString
|
|
571
|
-
let aboveRange = nsString.range(of: "Above")
|
|
572
|
-
let hrRange = nsString.range(of: "\u{FFFC}")
|
|
573
|
-
guard aboveRange.location != NSNotFound, hrRange.location != NSNotFound else {
|
|
574
|
-
XCTFail("expected both paragraph text and horizontal rule in rendered output")
|
|
575
|
-
return
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
let aboveParagraphStyle = result.attribute(.paragraphStyle, at: aboveRange.location, effectiveRange: nil)
|
|
579
|
-
as? NSParagraphStyle
|
|
580
|
-
let separatorParagraphStyle = result.attribute(
|
|
581
|
-
.paragraphStyle,
|
|
582
|
-
at: hrRange.location + hrRange.length,
|
|
583
|
-
effectiveRange: nil
|
|
584
|
-
) as? NSParagraphStyle
|
|
585
|
-
let attachment = result.attribute(.attachment, at: hrRange.location, effectiveRange: nil)
|
|
586
|
-
as? HorizontalRuleAttachment
|
|
587
|
-
|
|
588
|
-
XCTAssertEqual(attachment?.verticalPadding ?? 0, 10, accuracy: 0.1)
|
|
589
|
-
XCTAssertEqual(aboveParagraphStyle?.paragraphSpacing ?? -1, 4, accuracy: 0.1)
|
|
590
|
-
XCTAssertEqual(separatorParagraphStyle?.paragraphSpacing ?? -1, 4, accuracy: 0.1)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// MARK: - Multiple Paragraphs
|
|
594
|
-
|
|
595
|
-
/// Two consecutive paragraphs should be separated by a newline.
|
|
596
|
-
func testRender_multipleParagraphs() {
|
|
597
|
-
let json = """
|
|
598
|
-
[
|
|
599
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
600
|
-
{"type": "textRun", "text": "First", "marks": []},
|
|
601
|
-
{"type": "blockEnd"},
|
|
602
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
603
|
-
{"type": "textRun", "text": "Second", "marks": []},
|
|
604
|
-
{"type": "blockEnd"}
|
|
605
|
-
]
|
|
606
|
-
"""
|
|
607
|
-
let result = RenderBridge.renderElements(
|
|
608
|
-
fromJSON: json,
|
|
609
|
-
baseFont: baseFont,
|
|
610
|
-
textColor: textColor
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
XCTAssertEqual(
|
|
614
|
-
result.string, "First\nSecond",
|
|
615
|
-
"Two paragraphs should be separated by a newline"
|
|
616
|
-
)
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// MARK: - Mixed Marks in Same Paragraph
|
|
620
|
-
|
|
621
|
-
/// A paragraph with mixed styled runs should produce the correct combined string
|
|
622
|
-
/// with different attributes at different ranges.
|
|
623
|
-
func testRender_mixedMarksInParagraph() {
|
|
624
|
-
let json = """
|
|
625
|
-
[
|
|
626
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
627
|
-
{"type": "textRun", "text": "normal ", "marks": []},
|
|
628
|
-
{"type": "textRun", "text": "bold", "marks": ["bold"]},
|
|
629
|
-
{"type": "textRun", "text": " end", "marks": []},
|
|
630
|
-
{"type": "blockEnd"}
|
|
631
|
-
]
|
|
632
|
-
"""
|
|
633
|
-
let result = RenderBridge.renderElements(
|
|
634
|
-
fromJSON: json,
|
|
635
|
-
baseFont: baseFont,
|
|
636
|
-
textColor: textColor
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
XCTAssertEqual(result.string, "normal bold end")
|
|
640
|
-
|
|
641
|
-
// Check "normal " (offset 0) has base font, not bold.
|
|
642
|
-
let normalAttrs = result.attributes(at: 0, effectiveRange: nil)
|
|
643
|
-
let normalFont = normalAttrs[.font] as? UIFont
|
|
644
|
-
XCTAssertFalse(
|
|
645
|
-
normalFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? true,
|
|
646
|
-
"'normal' should not be bold"
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
// Check "bold" (offset 7) has bold font.
|
|
650
|
-
let boldAttrs = result.attributes(at: 7, effectiveRange: nil)
|
|
651
|
-
let boldFont = boldAttrs[.font] as? UIFont
|
|
652
|
-
XCTAssertTrue(
|
|
653
|
-
boldFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
654
|
-
"'bold' should have bold trait"
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
// Check " end" (offset 11) has base font, not bold.
|
|
658
|
-
let endAttrs = result.attributes(at: 11, effectiveRange: nil)
|
|
659
|
-
let endFont = endAttrs[.font] as? UIFont
|
|
660
|
-
XCTAssertFalse(
|
|
661
|
-
endFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? true,
|
|
662
|
-
"'end' should not be bold"
|
|
663
|
-
)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// MARK: - Ordered List
|
|
667
|
-
|
|
668
|
-
/// Ordered list items should reserve gutter space without injecting marker text.
|
|
669
|
-
func testRender_orderedListItem() {
|
|
670
|
-
let json = """
|
|
671
|
-
[
|
|
672
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
673
|
-
"listContext": {"ordered": true, "index": 1, "total": 2, "start": 1, "isFirst": true, "isLast": false}},
|
|
674
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
675
|
-
{"type": "textRun", "text": "First item", "marks": []},
|
|
676
|
-
{"type": "blockEnd"},
|
|
677
|
-
{"type": "blockEnd"},
|
|
678
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
679
|
-
"listContext": {"ordered": true, "index": 2, "total": 2, "start": 1, "isFirst": false, "isLast": true}},
|
|
680
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
681
|
-
{"type": "textRun", "text": "Second item", "marks": []},
|
|
682
|
-
{"type": "blockEnd"},
|
|
683
|
-
{"type": "blockEnd"}
|
|
684
|
-
]
|
|
685
|
-
"""
|
|
686
|
-
let result = RenderBridge.renderElements(
|
|
687
|
-
fromJSON: json,
|
|
688
|
-
baseFont: baseFont,
|
|
689
|
-
textColor: textColor
|
|
690
|
-
)
|
|
691
|
-
|
|
692
|
-
XCTAssertEqual(result.string, "First item\nSecond item")
|
|
693
|
-
|
|
694
|
-
let firstAttrs = result.attributes(at: 0, effectiveRange: nil)
|
|
695
|
-
let firstStyle = firstAttrs[.paragraphStyle] as? NSParagraphStyle
|
|
696
|
-
XCTAssertNotNil(firstAttrs[RenderBridgeAttributes.listContext])
|
|
697
|
-
XCTAssertEqual(firstStyle?.firstLineHeadIndent, 68.0)
|
|
698
|
-
XCTAssertEqual(firstStyle?.headIndent, 68.0)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// MARK: - Unordered List
|
|
702
|
-
|
|
703
|
-
func testRender_unorderedListItem() {
|
|
704
|
-
let json = """
|
|
705
|
-
[
|
|
706
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
707
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
708
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
709
|
-
{"type": "textRun", "text": "Bullet item", "marks": []},
|
|
710
|
-
{"type": "blockEnd"},
|
|
711
|
-
{"type": "blockEnd"}
|
|
712
|
-
]
|
|
713
|
-
"""
|
|
714
|
-
let result = RenderBridge.renderElements(
|
|
715
|
-
fromJSON: json,
|
|
716
|
-
baseFont: baseFont,
|
|
717
|
-
textColor: textColor
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
XCTAssertEqual(result.string, "Bullet item")
|
|
721
|
-
XCTAssertNotNil(result.attribute(RenderBridgeAttributes.listContext, at: 0, effectiveRange: nil))
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
func testRender_unorderedListMarkerUsesLargerFontThanItemText() {
|
|
725
|
-
let json = """
|
|
726
|
-
[
|
|
727
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
728
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
729
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
730
|
-
{"type": "textRun", "text": "Bullet item", "marks": []},
|
|
731
|
-
{"type": "blockEnd"},
|
|
732
|
-
{"type": "blockEnd"}
|
|
733
|
-
]
|
|
734
|
-
"""
|
|
735
|
-
let result = RenderBridge.renderElements(
|
|
736
|
-
fromJSON: json,
|
|
737
|
-
baseFont: baseFont,
|
|
738
|
-
textColor: textColor
|
|
739
|
-
)
|
|
740
|
-
|
|
741
|
-
let textFont = result.attribute(.font, at: 0, effectiveRange: nil) as? UIFont
|
|
742
|
-
XCTAssertEqual(textFont?.pointSize, baseFont.pointSize)
|
|
743
|
-
XCTAssertNotNil(result.attribute(RenderBridgeAttributes.listContext, at: 0, effectiveRange: nil))
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
func testRender_emptyUnorderedListItemDoesNotInsertParagraphNewlineAfterMarker() {
|
|
747
|
-
let json = """
|
|
748
|
-
[
|
|
749
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
750
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
751
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
752
|
-
{"type": "textRun", "text": "\\u200B", "marks": []},
|
|
753
|
-
{"type": "blockEnd"},
|
|
754
|
-
{"type": "blockEnd"}
|
|
755
|
-
]
|
|
756
|
-
"""
|
|
757
|
-
let result = RenderBridge.renderElements(
|
|
758
|
-
fromJSON: json,
|
|
759
|
-
baseFont: baseFont,
|
|
760
|
-
textColor: textColor
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
XCTAssertEqual(
|
|
764
|
-
result.string, "\u{200B}",
|
|
765
|
-
"An empty list item should render only its placeholder text. Got: '\(result.string)'"
|
|
766
|
-
)
|
|
767
|
-
XCTAssertNotNil(result.attribute(RenderBridgeAttributes.listContext, at: 0, effectiveRange: nil))
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
func testRender_emptyParagraphAfterListUsesItsOwnParagraphStyle() {
|
|
771
|
-
let json = """
|
|
772
|
-
[
|
|
773
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
774
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
775
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
776
|
-
{"type": "textRun", "text": "A", "marks": []},
|
|
777
|
-
{"type": "blockEnd"},
|
|
778
|
-
{"type": "blockEnd"},
|
|
779
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
780
|
-
{"type": "textRun", "text": "\\u200B", "marks": []},
|
|
781
|
-
{"type": "blockEnd"}
|
|
782
|
-
]
|
|
783
|
-
"""
|
|
784
|
-
let result = RenderBridge.renderElements(
|
|
785
|
-
fromJSON: json,
|
|
786
|
-
baseFont: baseFont,
|
|
787
|
-
textColor: textColor
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
XCTAssertEqual(result.string, "A\n\u{200B}")
|
|
791
|
-
|
|
792
|
-
let placeholderIndex = (result.string as NSString).length - 1
|
|
793
|
-
let placeholderStyle = result.attribute(
|
|
794
|
-
.paragraphStyle,
|
|
795
|
-
at: placeholderIndex,
|
|
796
|
-
effectiveRange: nil
|
|
797
|
-
) as? NSParagraphStyle
|
|
798
|
-
|
|
799
|
-
XCTAssertNotNil(placeholderStyle, "Empty paragraph placeholder should carry paragraph style")
|
|
800
|
-
XCTAssertEqual(placeholderStyle?.firstLineHeadIndent, 0)
|
|
801
|
-
XCTAssertEqual(placeholderStyle?.headIndent, 0)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
func testRender_secondParagraphInListItemDoesNotGetListMarkerContext() {
|
|
805
|
-
let json = """
|
|
806
|
-
[
|
|
807
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
808
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
809
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
810
|
-
{"type": "textRun", "text": "A", "marks": []},
|
|
811
|
-
{"type": "blockEnd"},
|
|
812
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
813
|
-
{"type": "textRun", "text": "\\u200B", "marks": []},
|
|
814
|
-
{"type": "blockEnd"},
|
|
815
|
-
{"type": "blockEnd"}
|
|
816
|
-
]
|
|
817
|
-
"""
|
|
818
|
-
let result = RenderBridge.renderElements(
|
|
819
|
-
fromJSON: json,
|
|
820
|
-
baseFont: baseFont,
|
|
821
|
-
textColor: textColor
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
XCTAssertNotNil(
|
|
825
|
-
result.attribute(RenderBridgeAttributes.listMarkerContext, at: 0, effectiveRange: nil),
|
|
826
|
-
"The first paragraph in a list item should keep its marker context"
|
|
827
|
-
)
|
|
828
|
-
XCTAssertNil(
|
|
829
|
-
result.attribute(RenderBridgeAttributes.listMarkerContext, at: 2, effectiveRange: nil),
|
|
830
|
-
"The second paragraph in a list item should not render a separate list marker"
|
|
831
|
-
)
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// MARK: - Invalid JSON
|
|
835
|
-
|
|
836
|
-
func testRender_invalidJSON() {
|
|
837
|
-
let result = RenderBridge.renderElements(
|
|
838
|
-
fromJSON: "not valid json",
|
|
839
|
-
baseFont: baseFont,
|
|
840
|
-
textColor: textColor
|
|
841
|
-
)
|
|
842
|
-
|
|
843
|
-
XCTAssertEqual(
|
|
844
|
-
result.string, "",
|
|
845
|
-
"Invalid JSON should produce empty attributed string"
|
|
846
|
-
)
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
func testRender_emptyArray() {
|
|
850
|
-
let result = RenderBridge.renderElements(
|
|
851
|
-
fromJSON: "[]",
|
|
852
|
-
baseFont: baseFont,
|
|
853
|
-
textColor: textColor
|
|
854
|
-
)
|
|
855
|
-
|
|
856
|
-
XCTAssertEqual(
|
|
857
|
-
result.string, "",
|
|
858
|
-
"Empty array should produce empty attributed string"
|
|
859
|
-
)
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// MARK: - Mark Attributes Isolated Tests
|
|
863
|
-
|
|
864
|
-
/// Test attributesForMarks directly to verify all mark combinations.
|
|
865
|
-
func testAttributesForMarks_noMarks() {
|
|
866
|
-
let attrs = RenderBridge.attributesForMarks([], baseFont: baseFont, textColor: textColor)
|
|
867
|
-
let font = attrs[.font] as? UIFont
|
|
868
|
-
XCTAssertEqual(font, baseFont, "No marks should use base font")
|
|
869
|
-
XCTAssertNil(attrs[.underlineStyle], "No marks should have no underline")
|
|
870
|
-
XCTAssertNil(attrs[.strikethroughStyle], "No marks should have no strikethrough")
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
func testAttributesForMarks_strongAlias() {
|
|
874
|
-
// "strong" is an alias for "bold"
|
|
875
|
-
let attrs = RenderBridge.attributesForMarks(
|
|
876
|
-
["strong"],
|
|
877
|
-
baseFont: baseFont,
|
|
878
|
-
textColor: textColor
|
|
879
|
-
)
|
|
880
|
-
let font = attrs[.font] as? UIFont
|
|
881
|
-
XCTAssertTrue(
|
|
882
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
883
|
-
"'strong' should produce bold font"
|
|
884
|
-
)
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
func testAttributesForMarks_emAlias() {
|
|
888
|
-
// "em" is an alias for "italic"
|
|
889
|
-
let attrs = RenderBridge.attributesForMarks(
|
|
890
|
-
["em"],
|
|
891
|
-
baseFont: baseFont,
|
|
892
|
-
textColor: textColor
|
|
893
|
-
)
|
|
894
|
-
let font = attrs[.font] as? UIFont
|
|
895
|
-
XCTAssertTrue(
|
|
896
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitItalic) ?? false,
|
|
897
|
-
"'em' should produce italic font"
|
|
898
|
-
)
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
func testAttributesForMarks_strikethroughAlias() {
|
|
902
|
-
// "strikethrough" is an alias for "strike"
|
|
903
|
-
let attrs = RenderBridge.attributesForMarks(
|
|
904
|
-
["strikethrough"],
|
|
905
|
-
baseFont: baseFont,
|
|
906
|
-
textColor: textColor
|
|
907
|
-
)
|
|
908
|
-
let strikethrough = attrs[.strikethroughStyle] as? Int
|
|
909
|
-
XCTAssertEqual(
|
|
910
|
-
strikethrough, NSUnderlineStyle.single.rawValue,
|
|
911
|
-
"'strikethrough' should produce strikethrough style"
|
|
912
|
-
)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
func testAttributesForMarks_allCombined() {
|
|
916
|
-
let attrs = RenderBridge.attributesForMarks(
|
|
917
|
-
["bold", "italic", "underline", "strike"],
|
|
918
|
-
baseFont: baseFont,
|
|
919
|
-
textColor: textColor
|
|
920
|
-
)
|
|
921
|
-
let font = attrs[.font] as? UIFont
|
|
922
|
-
let traits = font?.fontDescriptor.symbolicTraits ?? []
|
|
923
|
-
XCTAssertTrue(traits.contains(.traitBold), "Should have bold")
|
|
924
|
-
XCTAssertTrue(traits.contains(.traitItalic), "Should have italic")
|
|
925
|
-
XCTAssertEqual(
|
|
926
|
-
attrs[.underlineStyle] as? Int,
|
|
927
|
-
NSUnderlineStyle.single.rawValue,
|
|
928
|
-
"Should have underline"
|
|
929
|
-
)
|
|
930
|
-
XCTAssertEqual(
|
|
931
|
-
attrs[.strikethroughStyle] as? Int,
|
|
932
|
-
NSUnderlineStyle.single.rawValue,
|
|
933
|
-
"Should have strikethrough"
|
|
934
|
-
)
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
func testAttributesForMarks_unknownMarkIgnored() {
|
|
938
|
-
let attrs = RenderBridge.attributesForMarks(
|
|
939
|
-
["unknownMark"],
|
|
940
|
-
baseFont: baseFont,
|
|
941
|
-
textColor: textColor
|
|
942
|
-
)
|
|
943
|
-
let font = attrs[.font] as? UIFont
|
|
944
|
-
XCTAssertEqual(
|
|
945
|
-
font, baseFont,
|
|
946
|
-
"Unknown marks should be ignored, producing base font"
|
|
947
|
-
)
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// MARK: - Paragraph Style Tests
|
|
951
|
-
|
|
952
|
-
func testParagraphStyle_depth0() {
|
|
953
|
-
let ctx = BlockContext(nodeType: "paragraph", depth: 0, listContext: nil)
|
|
954
|
-
let style = RenderBridge.paragraphStyleForBlock(ctx, blockStack: [ctx])
|
|
955
|
-
XCTAssertEqual(
|
|
956
|
-
style.firstLineHeadIndent, 0,
|
|
957
|
-
"Depth 0 paragraph should have 0 indentation"
|
|
958
|
-
)
|
|
959
|
-
XCTAssertEqual(
|
|
960
|
-
style.headIndent, 0,
|
|
961
|
-
"Depth 0 paragraph should have 0 head indent"
|
|
962
|
-
)
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
func testParagraphStyle_depth2() {
|
|
966
|
-
let ctx = BlockContext(nodeType: "paragraph", depth: 2, listContext: nil)
|
|
967
|
-
let style = RenderBridge.paragraphStyleForBlock(ctx, blockStack: [ctx])
|
|
968
|
-
let expectedIndent: CGFloat = 2 * 24.0 // 2 * indentPerDepth
|
|
969
|
-
XCTAssertEqual(
|
|
970
|
-
style.firstLineHeadIndent, expectedIndent,
|
|
971
|
-
"Depth 2 paragraph should have \(expectedIndent) first line indent"
|
|
972
|
-
)
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
func testParagraphStyle_listItem() {
|
|
976
|
-
let listCtx: [String: Any] = [
|
|
977
|
-
"ordered": true,
|
|
978
|
-
"index": 1,
|
|
979
|
-
"total": 3,
|
|
980
|
-
"start": 1,
|
|
981
|
-
"isFirst": true,
|
|
982
|
-
"isLast": false,
|
|
983
|
-
]
|
|
984
|
-
let ctx = BlockContext(nodeType: "listItem", depth: 1, listContext: listCtx)
|
|
985
|
-
let style = RenderBridge.paragraphStyleForBlock(ctx, blockStack: [ctx])
|
|
986
|
-
|
|
987
|
-
let baseIndent: CGFloat = 1 * 24.0 // depth * indentPerDepth
|
|
988
|
-
XCTAssertEqual(
|
|
989
|
-
style.firstLineHeadIndent, baseIndent + 20.0,
|
|
990
|
-
"List item first line indent should reserve marker width"
|
|
991
|
-
)
|
|
992
|
-
XCTAssertEqual(
|
|
993
|
-
style.headIndent, baseIndent + 20.0, // + listMarkerWidth
|
|
994
|
-
"List item head indent should include marker width"
|
|
995
|
-
)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
func testParagraphStyle_listBaseIndentMultiplierCanCollapseTopLevelIndent() {
|
|
999
|
-
let listCtx: [String: Any] = [
|
|
1000
|
-
"ordered": false,
|
|
1001
|
-
"index": 1,
|
|
1002
|
-
"total": 1,
|
|
1003
|
-
"start": 1,
|
|
1004
|
-
"isFirst": true,
|
|
1005
|
-
"isLast": true,
|
|
1006
|
-
]
|
|
1007
|
-
let topLevelCtx = BlockContext(nodeType: "paragraph", depth: 1, listContext: listCtx)
|
|
1008
|
-
let nestedCtx = BlockContext(nodeType: "paragraph", depth: 2, listContext: listCtx)
|
|
1009
|
-
let theme = EditorTheme(dictionary: [
|
|
1010
|
-
"list": [
|
|
1011
|
-
"indent": 24,
|
|
1012
|
-
"baseIndentMultiplier": 0,
|
|
1013
|
-
],
|
|
1014
|
-
])
|
|
1015
|
-
|
|
1016
|
-
let topLevelStyle = RenderBridge.paragraphStyleForBlock(
|
|
1017
|
-
topLevelCtx,
|
|
1018
|
-
blockStack: [topLevelCtx],
|
|
1019
|
-
theme: theme,
|
|
1020
|
-
baseFont: baseFont
|
|
1021
|
-
)
|
|
1022
|
-
let nestedStyle = RenderBridge.paragraphStyleForBlock(
|
|
1023
|
-
nestedCtx,
|
|
1024
|
-
blockStack: [nestedCtx],
|
|
1025
|
-
theme: theme,
|
|
1026
|
-
baseFont: baseFont
|
|
1027
|
-
)
|
|
1028
|
-
|
|
1029
|
-
XCTAssertEqual(
|
|
1030
|
-
topLevelStyle.firstLineHeadIndent,
|
|
1031
|
-
LayoutConstants.listMarkerWidth,
|
|
1032
|
-
accuracy: 0.1,
|
|
1033
|
-
"Top-level list items should be flush-left apart from the marker gutter"
|
|
1034
|
-
)
|
|
1035
|
-
XCTAssertEqual(
|
|
1036
|
-
topLevelStyle.headIndent,
|
|
1037
|
-
LayoutConstants.listMarkerWidth,
|
|
1038
|
-
accuracy: 0.1,
|
|
1039
|
-
"Wrapped lines should align with the marker gutter when the base indent multiplier is zero"
|
|
1040
|
-
)
|
|
1041
|
-
XCTAssertEqual(
|
|
1042
|
-
nestedStyle.headIndent - topLevelStyle.headIndent,
|
|
1043
|
-
24,
|
|
1044
|
-
accuracy: 0.1,
|
|
1045
|
-
"Nested list levels should still add one indent unit each"
|
|
1046
|
-
)
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
func testParagraphStyle_unorderedMarkerScaleDoesNotWidenTextGutter() {
|
|
1050
|
-
let baseContext = BlockContext(
|
|
1051
|
-
nodeType: "listItem",
|
|
1052
|
-
depth: 1,
|
|
1053
|
-
listContext: [
|
|
1054
|
-
"ordered": false,
|
|
1055
|
-
"index": 1,
|
|
1056
|
-
"total": 1,
|
|
1057
|
-
"start": 1,
|
|
1058
|
-
"isFirst": true,
|
|
1059
|
-
"isLast": true,
|
|
1060
|
-
]
|
|
1061
|
-
)
|
|
1062
|
-
let baseTheme = EditorTheme(dictionary: [
|
|
1063
|
-
"list": [
|
|
1064
|
-
"indent": 24,
|
|
1065
|
-
"markerScale": 1,
|
|
1066
|
-
],
|
|
1067
|
-
])
|
|
1068
|
-
let scaledTheme = EditorTheme(dictionary: [
|
|
1069
|
-
"list": [
|
|
1070
|
-
"indent": 24,
|
|
1071
|
-
"markerScale": 2,
|
|
1072
|
-
],
|
|
1073
|
-
])
|
|
1074
|
-
|
|
1075
|
-
let largeBaseFont = UIFont.systemFont(ofSize: 40)
|
|
1076
|
-
let baseStyle = RenderBridge.paragraphStyleForBlock(
|
|
1077
|
-
baseContext,
|
|
1078
|
-
blockStack: [baseContext],
|
|
1079
|
-
theme: baseTheme,
|
|
1080
|
-
baseFont: largeBaseFont
|
|
1081
|
-
)
|
|
1082
|
-
let scaledStyle = RenderBridge.paragraphStyleForBlock(
|
|
1083
|
-
baseContext,
|
|
1084
|
-
blockStack: [baseContext],
|
|
1085
|
-
theme: scaledTheme,
|
|
1086
|
-
baseFont: largeBaseFont
|
|
1087
|
-
)
|
|
1088
|
-
|
|
1089
|
-
XCTAssertEqual(baseStyle.headIndent, scaledStyle.headIndent, accuracy: 0.1)
|
|
1090
|
-
XCTAssertEqual(baseStyle.firstLineHeadIndent, scaledStyle.firstLineHeadIndent, accuracy: 0.1)
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
func testParagraphStyle_blockquoteUsesQuoteIndent() {
|
|
1094
|
-
let quote = BlockContext(nodeType: "blockquote", depth: 0, listContext: nil)
|
|
1095
|
-
let paragraph = BlockContext(nodeType: "paragraph", depth: 1, listContext: nil)
|
|
1096
|
-
let theme = EditorTheme(dictionary: [
|
|
1097
|
-
"blockquote": [
|
|
1098
|
-
"indent": 20,
|
|
1099
|
-
"borderColor": "#aa5500",
|
|
1100
|
-
"borderWidth": 4,
|
|
1101
|
-
"markerGap": 10,
|
|
1102
|
-
],
|
|
1103
|
-
])
|
|
1104
|
-
|
|
1105
|
-
let style = RenderBridge.paragraphStyleForBlock(
|
|
1106
|
-
paragraph,
|
|
1107
|
-
blockStack: [quote, paragraph],
|
|
1108
|
-
theme: theme,
|
|
1109
|
-
baseFont: baseFont
|
|
1110
|
-
)
|
|
1111
|
-
|
|
1112
|
-
XCTAssertEqual(style.firstLineHeadIndent, 20, accuracy: 0.1)
|
|
1113
|
-
XCTAssertEqual(style.headIndent, 20, accuracy: 0.1)
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
func testParagraphStyle_nestedListItemInsideBlockquoteAddsListIndent() {
|
|
1117
|
-
let quote = BlockContext(nodeType: "blockquote", depth: 0, listContext: nil)
|
|
1118
|
-
let parentListItem = BlockContext(
|
|
1119
|
-
nodeType: "listItem",
|
|
1120
|
-
depth: 1,
|
|
1121
|
-
listContext: ["ordered": false, "index": 1, "total": 2, "start": 1, "isFirst": true, "isLast": false]
|
|
1122
|
-
)
|
|
1123
|
-
let parentParagraph = BlockContext(nodeType: "paragraph", depth: 2, listContext: nil)
|
|
1124
|
-
let nestedListItem = BlockContext(
|
|
1125
|
-
nodeType: "listItem",
|
|
1126
|
-
depth: 2,
|
|
1127
|
-
listContext: ["ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true]
|
|
1128
|
-
)
|
|
1129
|
-
let nestedParagraph = BlockContext(nodeType: "paragraph", depth: 3, listContext: nil)
|
|
1130
|
-
|
|
1131
|
-
let parentStyle = RenderBridge.paragraphStyleForBlock(
|
|
1132
|
-
parentParagraph,
|
|
1133
|
-
blockStack: [quote, parentListItem, parentParagraph],
|
|
1134
|
-
theme: nil,
|
|
1135
|
-
baseFont: baseFont
|
|
1136
|
-
)
|
|
1137
|
-
let nestedStyle = RenderBridge.paragraphStyleForBlock(
|
|
1138
|
-
nestedParagraph,
|
|
1139
|
-
blockStack: [quote, parentListItem, nestedListItem, nestedParagraph],
|
|
1140
|
-
theme: nil,
|
|
1141
|
-
baseFont: baseFont
|
|
1142
|
-
)
|
|
1143
|
-
|
|
1144
|
-
XCTAssertGreaterThan(
|
|
1145
|
-
nestedStyle.headIndent,
|
|
1146
|
-
parentStyle.headIndent,
|
|
1147
|
-
"nested list item inside a blockquote should indent more than its parent item"
|
|
1148
|
-
)
|
|
1149
|
-
XCTAssertGreaterThan(
|
|
1150
|
-
nestedStyle.firstLineHeadIndent,
|
|
1151
|
-
parentStyle.firstLineHeadIndent,
|
|
1152
|
-
"nested list marker should also move inward inside a blockquote"
|
|
1153
|
-
)
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
func testParagraphStyle_firstLevelListInsideBlockquoteAddsListIndentInsideQuote() {
|
|
1157
|
-
let json = """
|
|
1158
|
-
[
|
|
1159
|
-
{"type": "blockStart", "nodeType": "blockquote", "depth": 0},
|
|
1160
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
1161
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
1162
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
1163
|
-
{"type": "textRun", "text": "Quoted item", "marks": []},
|
|
1164
|
-
{"type": "blockEnd"},
|
|
1165
|
-
{"type": "blockEnd"},
|
|
1166
|
-
{"type": "blockEnd"}
|
|
1167
|
-
]
|
|
1168
|
-
"""
|
|
1169
|
-
let result = RenderBridge.renderElements(
|
|
1170
|
-
fromJSON: json,
|
|
1171
|
-
baseFont: baseFont,
|
|
1172
|
-
textColor: textColor
|
|
1173
|
-
)
|
|
1174
|
-
let style = result.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
|
|
1175
|
-
let quote = BlockContext(nodeType: "blockquote", depth: 0, listContext: nil)
|
|
1176
|
-
let quotedParagraph = BlockContext(nodeType: "paragraph", depth: 1, listContext: nil)
|
|
1177
|
-
let quotedListParagraph = BlockContext(
|
|
1178
|
-
nodeType: "paragraph",
|
|
1179
|
-
depth: 2,
|
|
1180
|
-
listContext: ["ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true]
|
|
1181
|
-
)
|
|
1182
|
-
let plainQuotedStyle = RenderBridge.paragraphStyleForBlock(
|
|
1183
|
-
quotedParagraph,
|
|
1184
|
-
blockStack: [quote, quotedParagraph],
|
|
1185
|
-
theme: nil,
|
|
1186
|
-
baseFont: baseFont
|
|
1187
|
-
)
|
|
1188
|
-
let expectedStyle = RenderBridge.paragraphStyleForBlock(
|
|
1189
|
-
quotedListParagraph,
|
|
1190
|
-
blockStack: [quote, quotedListParagraph],
|
|
1191
|
-
theme: nil,
|
|
1192
|
-
baseFont: baseFont
|
|
1193
|
-
)
|
|
1194
|
-
|
|
1195
|
-
XCTAssertEqual(
|
|
1196
|
-
style?.headIndent ?? -1,
|
|
1197
|
-
expectedStyle.headIndent,
|
|
1198
|
-
accuracy: 0.1,
|
|
1199
|
-
"first-level list paragraphs inside a blockquote should keep their extra list indent"
|
|
1200
|
-
)
|
|
1201
|
-
XCTAssertEqual(
|
|
1202
|
-
style?.firstLineHeadIndent ?? -1,
|
|
1203
|
-
expectedStyle.firstLineHeadIndent,
|
|
1204
|
-
accuracy: 0.1,
|
|
1205
|
-
"first-level quoted list markers should keep their extra list indent"
|
|
1206
|
-
)
|
|
1207
|
-
XCTAssertGreaterThan(
|
|
1208
|
-
style?.headIndent ?? -1,
|
|
1209
|
-
plainQuotedStyle.headIndent,
|
|
1210
|
-
"quoted list text should indent further than plain quoted text"
|
|
1211
|
-
)
|
|
1212
|
-
XCTAssertGreaterThan(
|
|
1213
|
-
style?.firstLineHeadIndent ?? -1,
|
|
1214
|
-
plainQuotedStyle.firstLineHeadIndent,
|
|
1215
|
-
"quoted list marker gutter should indent further than plain quoted text"
|
|
1216
|
-
)
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
func testRender_blockquoteAppliesBorderAttributesAndTextTheme() {
|
|
1220
|
-
let json = """
|
|
1221
|
-
[
|
|
1222
|
-
{"type": "blockStart", "nodeType": "blockquote", "depth": 0},
|
|
1223
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1224
|
-
{"type": "textRun", "text": "Quoted", "marks": []},
|
|
1225
|
-
{"type": "blockEnd"},
|
|
1226
|
-
{"type": "blockEnd"}
|
|
1227
|
-
]
|
|
1228
|
-
"""
|
|
1229
|
-
let result = RenderBridge.renderElements(
|
|
1230
|
-
fromJSON: json,
|
|
1231
|
-
baseFont: baseFont,
|
|
1232
|
-
textColor: textColor,
|
|
1233
|
-
theme: EditorTheme(dictionary: [
|
|
1234
|
-
"blockquote": [
|
|
1235
|
-
"indent": 20,
|
|
1236
|
-
"borderColor": "#aa5500",
|
|
1237
|
-
"borderWidth": 4,
|
|
1238
|
-
"markerGap": 10,
|
|
1239
|
-
"text": [
|
|
1240
|
-
"color": "#334455",
|
|
1241
|
-
],
|
|
1242
|
-
],
|
|
1243
|
-
])
|
|
1244
|
-
)
|
|
1245
|
-
let expectedTextColor = UIColor(
|
|
1246
|
-
red: 51.0 / 255.0,
|
|
1247
|
-
green: 68.0 / 255.0,
|
|
1248
|
-
blue: 85.0 / 255.0,
|
|
1249
|
-
alpha: 1
|
|
1250
|
-
)
|
|
1251
|
-
let expectedBorderColor = UIColor(
|
|
1252
|
-
red: 170.0 / 255.0,
|
|
1253
|
-
green: 85.0 / 255.0,
|
|
1254
|
-
blue: 0.0,
|
|
1255
|
-
alpha: 1
|
|
1256
|
-
)
|
|
1257
|
-
var foundStyledRun = false
|
|
1258
|
-
result.enumerateAttributes(
|
|
1259
|
-
in: NSRange(location: 0, length: result.length),
|
|
1260
|
-
options: []
|
|
1261
|
-
) { attrs, _, stop in
|
|
1262
|
-
guard attrs[RenderBridgeAttributes.blockquoteBorderColor] != nil else { return }
|
|
1263
|
-
XCTAssertEqual(attrs[.foregroundColor] as? UIColor, expectedTextColor)
|
|
1264
|
-
XCTAssertEqual(attrs[RenderBridgeAttributes.blockquoteBorderColor] as? UIColor, expectedBorderColor)
|
|
1265
|
-
XCTAssertEqual(
|
|
1266
|
-
(attrs[RenderBridgeAttributes.blockquoteBorderWidth] as? NSNumber)?.doubleValue ?? 0,
|
|
1267
|
-
4,
|
|
1268
|
-
accuracy: 0.1
|
|
1269
|
-
)
|
|
1270
|
-
XCTAssertEqual(
|
|
1271
|
-
(attrs[RenderBridgeAttributes.blockquoteMarkerGap] as? NSNumber)?.doubleValue ?? 0,
|
|
1272
|
-
10,
|
|
1273
|
-
accuracy: 0.1
|
|
1274
|
-
)
|
|
1275
|
-
foundStyledRun = true
|
|
1276
|
-
stop.pointee = true
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
XCTAssertTrue(foundStyledRun, "Expected a rendered run carrying blockquote border attributes")
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
func testRender_blockquoteDoesNotInsertExtraLeadingParagraphBreak() {
|
|
1283
|
-
let json = """
|
|
1284
|
-
[
|
|
1285
|
-
{"type": "blockStart", "nodeType": "blockquote", "depth": 0},
|
|
1286
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1287
|
-
{"type": "textRun", "text": "Hello", "marks": []},
|
|
1288
|
-
{"type": "blockEnd"},
|
|
1289
|
-
{"type": "blockEnd"},
|
|
1290
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1291
|
-
{"type": "textRun", "text": "World", "marks": []},
|
|
1292
|
-
{"type": "blockEnd"}
|
|
1293
|
-
]
|
|
1294
|
-
"""
|
|
1295
|
-
let result = RenderBridge.renderElements(
|
|
1296
|
-
fromJSON: json,
|
|
1297
|
-
baseFont: baseFont,
|
|
1298
|
-
textColor: textColor
|
|
1299
|
-
)
|
|
1300
|
-
|
|
1301
|
-
XCTAssertEqual(result.string, "Hello\nWorld")
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// MARK: - List Marker Generation
|
|
1305
|
-
|
|
1306
|
-
func testListMarker_ordered() {
|
|
1307
|
-
let ctx: [String: Any] = ["ordered": true, "index": 3]
|
|
1308
|
-
let marker = RenderBridge.listMarkerString(listContext: ctx)
|
|
1309
|
-
XCTAssertEqual(marker, "3. ", "Ordered list item 3 should produce '3. '")
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
func testListMarker_unordered() {
|
|
1313
|
-
let ctx: [String: Any] = ["ordered": false, "index": 1]
|
|
1314
|
-
let marker = RenderBridge.listMarkerString(listContext: ctx)
|
|
1315
|
-
XCTAssertEqual(marker, "\u{2022} ", "Unordered list should produce bullet + space")
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
func testListMarker_taskUnchecked() {
|
|
1319
|
-
let ctx: [String: Any] = ["kind": "task", "checked": false]
|
|
1320
|
-
let marker = RenderBridge.listMarkerString(listContext: ctx)
|
|
1321
|
-
XCTAssertEqual(marker, "\u{2610} ", "Unchecked task list should produce ballot box + space")
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// MARK: - Opaque Atoms
|
|
1325
|
-
|
|
1326
|
-
func testRender_opaqueInlineAtom() {
|
|
1327
|
-
let json = """
|
|
1328
|
-
[
|
|
1329
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1330
|
-
{"type": "textRun", "text": "before ", "marks": []},
|
|
1331
|
-
{"type": "opaqueInlineAtom", "label": "widget", "docPos": 8},
|
|
1332
|
-
{"type": "textRun", "text": " after", "marks": []},
|
|
1333
|
-
{"type": "blockEnd"}
|
|
1334
|
-
]
|
|
1335
|
-
"""
|
|
1336
|
-
let result = RenderBridge.renderElements(
|
|
1337
|
-
fromJSON: json,
|
|
1338
|
-
baseFont: baseFont,
|
|
1339
|
-
textColor: textColor
|
|
1340
|
-
)
|
|
1341
|
-
|
|
1342
|
-
XCTAssertTrue(
|
|
1343
|
-
result.string.contains("[widget]"),
|
|
1344
|
-
"Opaque inline atom should render as '[widget]'. Got: '\(result.string)'"
|
|
1345
|
-
)
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
func testRender_mentionInlineAtomUsesVisibleLabelAndTheme() {
|
|
1349
|
-
let json = """
|
|
1350
|
-
[
|
|
1351
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1352
|
-
{"type": "textRun", "text": "Hello ", "marks": []},
|
|
1353
|
-
{"type": "opaqueInlineAtom", "nodeType": "mention", "label": "@Alice", "docPos": 7},
|
|
1354
|
-
{"type": "textRun", "text": "!", "marks": []},
|
|
1355
|
-
{"type": "blockEnd"}
|
|
1356
|
-
]
|
|
1357
|
-
"""
|
|
1358
|
-
let theme = EditorTheme(dictionary: [
|
|
1359
|
-
"mentions": [
|
|
1360
|
-
"textColor": "#112233",
|
|
1361
|
-
"backgroundColor": "#ddeeff",
|
|
1362
|
-
"fontWeight": "bold",
|
|
1363
|
-
],
|
|
1364
|
-
])
|
|
1365
|
-
let result = RenderBridge.renderElements(
|
|
1366
|
-
fromJSON: json,
|
|
1367
|
-
baseFont: baseFont,
|
|
1368
|
-
textColor: textColor,
|
|
1369
|
-
theme: theme
|
|
1370
|
-
)
|
|
1371
|
-
|
|
1372
|
-
XCTAssertTrue(
|
|
1373
|
-
result.string.contains("@Alice"),
|
|
1374
|
-
"Mention inline atom should render its visible label. Got: '\(result.string)'"
|
|
1375
|
-
)
|
|
1376
|
-
XCTAssertFalse(
|
|
1377
|
-
result.string.contains("[@Alice]"),
|
|
1378
|
-
"Mention inline atom should not render using generic opaque brackets. Got: '\(result.string)'"
|
|
1379
|
-
)
|
|
1380
|
-
|
|
1381
|
-
let mentionRange = (result.string as NSString).range(of: "@Alice")
|
|
1382
|
-
XCTAssertNotEqual(mentionRange.location, NSNotFound)
|
|
1383
|
-
|
|
1384
|
-
let attrs = result.attributes(at: mentionRange.location, effectiveRange: nil)
|
|
1385
|
-
XCTAssertEqual(
|
|
1386
|
-
attrs[.foregroundColor] as? UIColor,
|
|
1387
|
-
UIColor(
|
|
1388
|
-
red: 0x11 as CGFloat / 255.0,
|
|
1389
|
-
green: 0x22 as CGFloat / 255.0,
|
|
1390
|
-
blue: 0x33 as CGFloat / 255.0,
|
|
1391
|
-
alpha: 1.0
|
|
1392
|
-
)
|
|
1393
|
-
)
|
|
1394
|
-
XCTAssertEqual(
|
|
1395
|
-
attrs[.backgroundColor] as? UIColor,
|
|
1396
|
-
UIColor(
|
|
1397
|
-
red: 0xdd as CGFloat / 255.0,
|
|
1398
|
-
green: 0xee as CGFloat / 255.0,
|
|
1399
|
-
blue: 0xff as CGFloat / 255.0,
|
|
1400
|
-
alpha: 1.0
|
|
1401
|
-
)
|
|
1402
|
-
)
|
|
1403
|
-
let font = attrs[.font] as? UIFont
|
|
1404
|
-
XCTAssertTrue(
|
|
1405
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
1406
|
-
"Mention theme should be able to request a bold font"
|
|
1407
|
-
)
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
func testRender_mentionInlineAtomMergesElementMentionThemeOverride() {
|
|
1411
|
-
let json = """
|
|
1412
|
-
[
|
|
1413
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1414
|
-
{
|
|
1415
|
-
"type": "opaqueInlineAtom",
|
|
1416
|
-
"nodeType": "mention",
|
|
1417
|
-
"label": "@Alice",
|
|
1418
|
-
"docPos": 1,
|
|
1419
|
-
"mentionTheme": {"textColor": "#445566"}
|
|
1420
|
-
},
|
|
1421
|
-
{"type": "blockEnd"}
|
|
1422
|
-
]
|
|
1423
|
-
"""
|
|
1424
|
-
let theme = EditorTheme(dictionary: [
|
|
1425
|
-
"mentions": [
|
|
1426
|
-
"textColor": "#112233",
|
|
1427
|
-
"backgroundColor": "#ddeeff",
|
|
1428
|
-
"fontWeight": "bold",
|
|
1429
|
-
],
|
|
1430
|
-
])
|
|
1431
|
-
let result = RenderBridge.renderElements(
|
|
1432
|
-
fromJSON: json,
|
|
1433
|
-
baseFont: baseFont,
|
|
1434
|
-
textColor: textColor,
|
|
1435
|
-
theme: theme
|
|
1436
|
-
)
|
|
1437
|
-
|
|
1438
|
-
XCTAssertEqual(result.string, "@Alice")
|
|
1439
|
-
|
|
1440
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
1441
|
-
XCTAssertEqual(
|
|
1442
|
-
attrs[.foregroundColor] as? UIColor,
|
|
1443
|
-
UIColor(
|
|
1444
|
-
red: 0x44 as CGFloat / 255.0,
|
|
1445
|
-
green: 0x55 as CGFloat / 255.0,
|
|
1446
|
-
blue: 0x66 as CGFloat / 255.0,
|
|
1447
|
-
alpha: 1.0
|
|
1448
|
-
)
|
|
1449
|
-
)
|
|
1450
|
-
XCTAssertEqual(
|
|
1451
|
-
attrs[.backgroundColor] as? UIColor,
|
|
1452
|
-
UIColor(
|
|
1453
|
-
red: 0xdd as CGFloat / 255.0,
|
|
1454
|
-
green: 0xee as CGFloat / 255.0,
|
|
1455
|
-
blue: 0xff as CGFloat / 255.0,
|
|
1456
|
-
alpha: 1.0
|
|
1457
|
-
)
|
|
1458
|
-
)
|
|
1459
|
-
let font = attrs[.font] as? UIFont
|
|
1460
|
-
XCTAssertTrue(
|
|
1461
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
1462
|
-
"Mention override should preserve global bold styling. Got: \(String(describing: font))"
|
|
1463
|
-
)
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
func testRender_opaqueBlockAtom() {
|
|
1467
|
-
let json = """
|
|
1468
|
-
[
|
|
1469
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1470
|
-
{"type": "textRun", "text": "Above", "marks": []},
|
|
1471
|
-
{"type": "blockEnd"},
|
|
1472
|
-
{"type": "opaqueBlockAtom", "label": "widgetBlock", "docPos": 7}
|
|
1473
|
-
]
|
|
1474
|
-
"""
|
|
1475
|
-
let result = RenderBridge.renderElements(
|
|
1476
|
-
fromJSON: json,
|
|
1477
|
-
baseFont: baseFont,
|
|
1478
|
-
textColor: textColor
|
|
1479
|
-
)
|
|
1480
|
-
|
|
1481
|
-
XCTAssertTrue(
|
|
1482
|
-
result.string.contains("[widgetBlock]"),
|
|
1483
|
-
"Opaque block atom should render as '[widgetBlock]'. Got: '\(result.string)'"
|
|
1484
|
-
)
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// MARK: - Theme Rendering
|
|
1488
|
-
|
|
1489
|
-
func testRender_themeOverridesParagraphTypography() {
|
|
1490
|
-
let json = """
|
|
1491
|
-
[
|
|
1492
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 0},
|
|
1493
|
-
{"type": "textRun", "text": "Styled", "marks": []},
|
|
1494
|
-
{"type": "blockEnd"}
|
|
1495
|
-
]
|
|
1496
|
-
"""
|
|
1497
|
-
let theme = EditorTheme(dictionary: [
|
|
1498
|
-
"text": [
|
|
1499
|
-
"fontFamily": "Courier",
|
|
1500
|
-
"fontSize": 18,
|
|
1501
|
-
"color": "#112233",
|
|
1502
|
-
],
|
|
1503
|
-
"paragraph": [
|
|
1504
|
-
"lineHeight": 28,
|
|
1505
|
-
"spacingAfter": 14,
|
|
1506
|
-
],
|
|
1507
|
-
])
|
|
1508
|
-
|
|
1509
|
-
let result = RenderBridge.renderElements(
|
|
1510
|
-
fromJSON: json,
|
|
1511
|
-
baseFont: baseFont,
|
|
1512
|
-
textColor: textColor,
|
|
1513
|
-
theme: theme
|
|
1514
|
-
)
|
|
1515
|
-
|
|
1516
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
1517
|
-
let font = attrs[.font] as? UIFont
|
|
1518
|
-
let color = attrs[.foregroundColor] as? UIColor
|
|
1519
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1520
|
-
|
|
1521
|
-
XCTAssertEqual(font?.pointSize ?? 0, 18, accuracy: 0.1)
|
|
1522
|
-
XCTAssertEqual(color, EditorTheme.color(from: "#112233"))
|
|
1523
|
-
XCTAssertEqual(paragraphStyle?.minimumLineHeight ?? 0, 28, accuracy: 0.1)
|
|
1524
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? 0, 14, accuracy: 0.1)
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
func testRender_themeOverridesSpecificHeadingLevelTypography() {
|
|
1528
|
-
let json = """
|
|
1529
|
-
[
|
|
1530
|
-
{"type": "blockStart", "nodeType": "h2", "depth": 0},
|
|
1531
|
-
{"type": "textRun", "text": "Section title", "marks": []},
|
|
1532
|
-
{"type": "blockEnd"}
|
|
1533
|
-
]
|
|
1534
|
-
"""
|
|
1535
|
-
let theme = EditorTheme(dictionary: [
|
|
1536
|
-
"text": [
|
|
1537
|
-
"fontSize": 16,
|
|
1538
|
-
"color": "#112233",
|
|
1539
|
-
],
|
|
1540
|
-
"headings": [
|
|
1541
|
-
"h2": [
|
|
1542
|
-
"fontSize": 28,
|
|
1543
|
-
"fontWeight": "700",
|
|
1544
|
-
"color": "#445566",
|
|
1545
|
-
"lineHeight": 34,
|
|
1546
|
-
"spacingAfter": 12,
|
|
1547
|
-
],
|
|
1548
|
-
"h4": [
|
|
1549
|
-
"fontSize": 18,
|
|
1550
|
-
"color": "#AA5500",
|
|
1551
|
-
],
|
|
1552
|
-
],
|
|
1553
|
-
])
|
|
1554
|
-
|
|
1555
|
-
let result = RenderBridge.renderElements(
|
|
1556
|
-
fromJSON: json,
|
|
1557
|
-
baseFont: baseFont,
|
|
1558
|
-
textColor: textColor,
|
|
1559
|
-
theme: theme
|
|
1560
|
-
)
|
|
1561
|
-
|
|
1562
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
1563
|
-
let font = attrs[.font] as? UIFont
|
|
1564
|
-
let color = attrs[.foregroundColor] as? UIColor
|
|
1565
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1566
|
-
|
|
1567
|
-
XCTAssertEqual(font?.pointSize ?? 0, 28, accuracy: 0.1)
|
|
1568
|
-
XCTAssertTrue(
|
|
1569
|
-
font?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
1570
|
-
"Configured h2 heading should resolve to a bold font"
|
|
1571
|
-
)
|
|
1572
|
-
XCTAssertEqual(color, EditorTheme.color(from: "#445566"))
|
|
1573
|
-
XCTAssertEqual(paragraphStyle?.minimumLineHeight ?? 0, 34, accuracy: 0.1)
|
|
1574
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? 0, 12, accuracy: 0.1)
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
func testRender_listItemUsesListItemSpacingWhenParagraphSpacingUnset() {
|
|
1578
|
-
let json = """
|
|
1579
|
-
[
|
|
1580
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1581
|
-
"listContext": {"ordered": false, "index": 1, "total": 2, "start": 1, "isFirst": true, "isLast": false}},
|
|
1582
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1583
|
-
{"type": "textRun", "text": "First item", "marks": []},
|
|
1584
|
-
{"type": "blockEnd"},
|
|
1585
|
-
{"type": "blockEnd"},
|
|
1586
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1587
|
-
"listContext": {"ordered": false, "index": 2, "total": 2, "start": 1, "isFirst": false, "isLast": true}},
|
|
1588
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1589
|
-
{"type": "textRun", "text": "Second item", "marks": []},
|
|
1590
|
-
{"type": "blockEnd"},
|
|
1591
|
-
{"type": "blockEnd"}
|
|
1592
|
-
]
|
|
1593
|
-
"""
|
|
1594
|
-
let theme = EditorTheme(dictionary: [
|
|
1595
|
-
"list": [
|
|
1596
|
-
"itemSpacing": 14,
|
|
1597
|
-
],
|
|
1598
|
-
])
|
|
1599
|
-
|
|
1600
|
-
let result = RenderBridge.renderElements(
|
|
1601
|
-
fromJSON: json,
|
|
1602
|
-
baseFont: baseFont,
|
|
1603
|
-
textColor: textColor,
|
|
1604
|
-
theme: theme
|
|
1605
|
-
)
|
|
1606
|
-
|
|
1607
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
1608
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1609
|
-
|
|
1610
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? 0, 14, accuracy: 0.1)
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
func testRender_listItemSpacingOverridesParagraphSpacingForSiblingListItems() {
|
|
1614
|
-
let json = """
|
|
1615
|
-
[
|
|
1616
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1617
|
-
"listContext": {"ordered": false, "index": 1, "total": 2, "start": 1, "isFirst": true, "isLast": false}},
|
|
1618
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1619
|
-
{"type": "textRun", "text": "First item", "marks": []},
|
|
1620
|
-
{"type": "blockEnd"},
|
|
1621
|
-
{"type": "blockEnd"},
|
|
1622
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1623
|
-
"listContext": {"ordered": false, "index": 2, "total": 2, "start": 1, "isFirst": false, "isLast": true}},
|
|
1624
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1625
|
-
{"type": "textRun", "text": "Second item", "marks": []},
|
|
1626
|
-
{"type": "blockEnd"},
|
|
1627
|
-
{"type": "blockEnd"}
|
|
1628
|
-
]
|
|
1629
|
-
"""
|
|
1630
|
-
let theme = EditorTheme(dictionary: [
|
|
1631
|
-
"paragraph": [
|
|
1632
|
-
"spacingAfter": 14,
|
|
1633
|
-
],
|
|
1634
|
-
"list": [
|
|
1635
|
-
"itemSpacing": 6,
|
|
1636
|
-
],
|
|
1637
|
-
])
|
|
1638
|
-
|
|
1639
|
-
let result = RenderBridge.renderElements(
|
|
1640
|
-
fromJSON: json,
|
|
1641
|
-
baseFont: baseFont,
|
|
1642
|
-
textColor: textColor,
|
|
1643
|
-
theme: theme
|
|
1644
|
-
)
|
|
1645
|
-
|
|
1646
|
-
let nsString = result.string as NSString
|
|
1647
|
-
let firstRange = nsString.range(of: "First item")
|
|
1648
|
-
XCTAssertNotEqual(firstRange.location, NSNotFound)
|
|
1649
|
-
|
|
1650
|
-
let attrs = result.attributes(at: firstRange.location, effectiveRange: nil)
|
|
1651
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1652
|
-
|
|
1653
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? -1, 6, accuracy: 0.1)
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
func testRender_nestedFirstListItemDoesNotKeepParentParagraphSpacingWhenItemSpacingIsZero() {
|
|
1657
|
-
let json = """
|
|
1658
|
-
[
|
|
1659
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1660
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
1661
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1662
|
-
{"type": "textRun", "text": "Parent item", "marks": []},
|
|
1663
|
-
{"type": "blockEnd"},
|
|
1664
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
1665
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
1666
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
1667
|
-
{"type": "textRun", "text": "Nested item", "marks": []},
|
|
1668
|
-
{"type": "blockEnd"},
|
|
1669
|
-
{"type": "blockEnd"},
|
|
1670
|
-
{"type": "blockEnd"}
|
|
1671
|
-
]
|
|
1672
|
-
"""
|
|
1673
|
-
let theme = EditorTheme(dictionary: [
|
|
1674
|
-
"paragraph": [
|
|
1675
|
-
"spacingAfter": 14,
|
|
1676
|
-
],
|
|
1677
|
-
"list": [
|
|
1678
|
-
"itemSpacing": 0,
|
|
1679
|
-
],
|
|
1680
|
-
])
|
|
1681
|
-
|
|
1682
|
-
let result = RenderBridge.renderElements(
|
|
1683
|
-
fromJSON: json,
|
|
1684
|
-
baseFont: baseFont,
|
|
1685
|
-
textColor: textColor,
|
|
1686
|
-
theme: theme
|
|
1687
|
-
)
|
|
1688
|
-
|
|
1689
|
-
let nsString = result.string as NSString
|
|
1690
|
-
let parentRange = nsString.range(of: "Parent item")
|
|
1691
|
-
XCTAssertNotEqual(parentRange.location, NSNotFound)
|
|
1692
|
-
|
|
1693
|
-
let attrs = result.attributes(at: parentRange.location, effectiveRange: nil)
|
|
1694
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1695
|
-
|
|
1696
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? -1, 0, accuracy: 0.1)
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
func testRender_nestedSiblingListItemsUseListItemSpacingInsteadOfParagraphSpacing() {
|
|
1700
|
-
let json = """
|
|
1701
|
-
[
|
|
1702
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1703
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
1704
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1705
|
-
{"type": "textRun", "text": "Parent item", "marks": []},
|
|
1706
|
-
{"type": "blockEnd"},
|
|
1707
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
1708
|
-
"listContext": {"ordered": false, "index": 1, "total": 2, "start": 1, "isFirst": true, "isLast": false}},
|
|
1709
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
1710
|
-
{"type": "textRun", "text": "Child one", "marks": []},
|
|
1711
|
-
{"type": "blockEnd"},
|
|
1712
|
-
{"type": "blockEnd"},
|
|
1713
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
1714
|
-
"listContext": {"ordered": false, "index": 2, "total": 2, "start": 1, "isFirst": false, "isLast": true}},
|
|
1715
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
1716
|
-
{"type": "textRun", "text": "Child two", "marks": []},
|
|
1717
|
-
{"type": "blockEnd"},
|
|
1718
|
-
{"type": "blockEnd"},
|
|
1719
|
-
{"type": "blockEnd"}
|
|
1720
|
-
]
|
|
1721
|
-
"""
|
|
1722
|
-
let theme = EditorTheme(dictionary: [
|
|
1723
|
-
"paragraph": [
|
|
1724
|
-
"spacingAfter": 14,
|
|
1725
|
-
],
|
|
1726
|
-
"list": [
|
|
1727
|
-
"itemSpacing": 6,
|
|
1728
|
-
],
|
|
1729
|
-
])
|
|
1730
|
-
|
|
1731
|
-
let result = RenderBridge.renderElements(
|
|
1732
|
-
fromJSON: json,
|
|
1733
|
-
baseFont: baseFont,
|
|
1734
|
-
textColor: textColor,
|
|
1735
|
-
theme: theme
|
|
1736
|
-
)
|
|
1737
|
-
|
|
1738
|
-
let nsString = result.string as NSString
|
|
1739
|
-
let childRange = nsString.range(of: "Child one")
|
|
1740
|
-
XCTAssertNotEqual(childRange.location, NSNotFound)
|
|
1741
|
-
|
|
1742
|
-
let attrs = result.attributes(at: childRange.location, effectiveRange: nil)
|
|
1743
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
1744
|
-
|
|
1745
|
-
XCTAssertEqual(paragraphStyle?.paragraphSpacing ?? -1, 6, accuracy: 0.1)
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
func testRender_themeOverridesHorizontalRuleMetrics() {
|
|
1749
|
-
let json = """
|
|
1750
|
-
[
|
|
1751
|
-
{"type": "voidBlock", "nodeType": "horizontalRule", "docPos": 0}
|
|
1752
|
-
]
|
|
1753
|
-
"""
|
|
1754
|
-
let theme = EditorTheme(dictionary: [
|
|
1755
|
-
"horizontalRule": [
|
|
1756
|
-
"color": "#445566",
|
|
1757
|
-
"thickness": 3,
|
|
1758
|
-
"verticalMargin": 12,
|
|
1759
|
-
],
|
|
1760
|
-
])
|
|
1761
|
-
|
|
1762
|
-
let result = RenderBridge.renderElements(
|
|
1763
|
-
fromJSON: json,
|
|
1764
|
-
baseFont: baseFont,
|
|
1765
|
-
textColor: textColor,
|
|
1766
|
-
theme: theme
|
|
1767
|
-
)
|
|
1768
|
-
|
|
1769
|
-
let attachment = result.attribute(.attachment, at: 0, effectiveRange: nil)
|
|
1770
|
-
as? HorizontalRuleAttachment
|
|
1771
|
-
XCTAssertEqual(attachment?.lineColor, EditorTheme.color(from: "#445566"))
|
|
1772
|
-
XCTAssertEqual(attachment?.lineHeight ?? 0, 3, accuracy: 0.1)
|
|
1773
|
-
XCTAssertEqual(attachment?.verticalPadding ?? 0, 12, accuracy: 0.1)
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
func testListMarkerDrawingRectUsesParagraphLineBox() {
|
|
1777
|
-
let markerFont = baseFont
|
|
1778
|
-
let lineFragmentRect = CGRect(x: 24, y: 10, width: 160, height: 28)
|
|
1779
|
-
let usedRect = CGRect(x: 24, y: 14, width: 160, height: 19)
|
|
1780
|
-
let baselineY: CGFloat = 28.140625
|
|
1781
|
-
let rect = EditorLayoutManager.markerDrawingRect(
|
|
1782
|
-
usedRect: usedRect,
|
|
1783
|
-
lineFragmentRect: lineFragmentRect,
|
|
1784
|
-
markerWidth: 20,
|
|
1785
|
-
baselineY: baselineY,
|
|
1786
|
-
markerFont: markerFont,
|
|
1787
|
-
origin: CGPoint(x: 0, y: 0)
|
|
1788
|
-
)
|
|
1789
|
-
let typographicHeight = markerFont.ascender - markerFont.descender
|
|
1790
|
-
let leading = max(markerFont.lineHeight - typographicHeight, 0)
|
|
1791
|
-
let expectedY = baselineY - markerFont.ascender - (leading / 2.0)
|
|
1792
|
-
|
|
1793
|
-
XCTAssertEqual(rect.origin.x, 4, accuracy: 0.1)
|
|
1794
|
-
XCTAssertEqual(rect.origin.y, expectedY, accuracy: 0.1)
|
|
1795
|
-
XCTAssertEqual(rect.height, markerFont.lineHeight, accuracy: 0.1)
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
func testListMarkerDrawingRectUsesFullLineFragmentWhenGlyphsUseShorterRect() {
|
|
1799
|
-
let markerFont = baseFont.withSize(18)
|
|
1800
|
-
let lineFragmentRect = CGRect(x: 24, y: 8, width: 160, height: 32)
|
|
1801
|
-
let usedRect = CGRect(x: 24, y: 14, width: 160, height: 17)
|
|
1802
|
-
let baselineY: CGFloat = 30.140625
|
|
1803
|
-
let rect = EditorLayoutManager.markerDrawingRect(
|
|
1804
|
-
usedRect: usedRect,
|
|
1805
|
-
lineFragmentRect: lineFragmentRect,
|
|
1806
|
-
markerWidth: 20,
|
|
1807
|
-
baselineY: baselineY,
|
|
1808
|
-
markerFont: markerFont,
|
|
1809
|
-
origin: CGPoint(x: 0, y: 0)
|
|
1810
|
-
)
|
|
1811
|
-
let typographicHeight = markerFont.ascender - markerFont.descender
|
|
1812
|
-
let leading = max(markerFont.lineHeight - typographicHeight, 0)
|
|
1813
|
-
let expectedY = baselineY - markerFont.ascender - (leading / 2.0)
|
|
1814
|
-
|
|
1815
|
-
XCTAssertEqual(rect.origin.x, 4, accuracy: 0.1)
|
|
1816
|
-
XCTAssertEqual(rect.origin.y, expectedY, accuracy: 0.1)
|
|
1817
|
-
XCTAssertEqual(rect.height, markerFont.lineHeight, accuracy: 0.1)
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
func testListMarkerDrawingRectFallsBackToLineFragmentWhenUsedRectIsEmpty() {
|
|
1821
|
-
let markerFont = baseFont
|
|
1822
|
-
let lineFragmentRect = CGRect(x: 24, y: 10, width: 160, height: 28)
|
|
1823
|
-
let rect = EditorLayoutManager.markerDrawingRect(
|
|
1824
|
-
usedRect: CGRect(x: 24, y: 10, width: 160, height: 0),
|
|
1825
|
-
lineFragmentRect: lineFragmentRect,
|
|
1826
|
-
markerWidth: 20,
|
|
1827
|
-
baselineY: 28.140625,
|
|
1828
|
-
markerFont: markerFont,
|
|
1829
|
-
origin: CGPoint(x: 0, y: 0)
|
|
1830
|
-
)
|
|
1831
|
-
let typographicHeight = markerFont.ascender - markerFont.descender
|
|
1832
|
-
let leading = max(markerFont.lineHeight - typographicHeight, 0)
|
|
1833
|
-
let expectedY = 28.140625 - markerFont.ascender - (leading / 2.0)
|
|
1834
|
-
|
|
1835
|
-
XCTAssertEqual(rect.origin.x, 4, accuracy: 0.1)
|
|
1836
|
-
XCTAssertEqual(rect.origin.y, expectedY, accuracy: 0.1)
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
func testOrderedMarkerDrawingOriginAlignsToBaselineWithoutParagraphLineHeight() {
|
|
1840
|
-
let markerFont = baseFont
|
|
1841
|
-
let lineFragmentRect = CGRect(x: 24, y: 8, width: 160, height: 32)
|
|
1842
|
-
let usedRect = CGRect(x: 24, y: 14, width: 160, height: 19)
|
|
1843
|
-
let baselineY: CGFloat = 30.140625
|
|
1844
|
-
let markerText = "12. "
|
|
1845
|
-
|
|
1846
|
-
let point = EditorLayoutManager.orderedMarkerDrawingOrigin(
|
|
1847
|
-
usedRect: usedRect,
|
|
1848
|
-
lineFragmentRect: lineFragmentRect,
|
|
1849
|
-
markerWidth: 20,
|
|
1850
|
-
baselineY: baselineY,
|
|
1851
|
-
markerFont: markerFont,
|
|
1852
|
-
markerText: markerText,
|
|
1853
|
-
origin: .zero
|
|
1854
|
-
)
|
|
1855
|
-
let markerWidth = ceil(("12." as NSString).size(withAttributes: [
|
|
1856
|
-
.font: markerFont,
|
|
1857
|
-
]).width)
|
|
1858
|
-
|
|
1859
|
-
XCTAssertEqual(point.x, usedRect.minX - 4.0 - markerWidth, accuracy: 0.1)
|
|
1860
|
-
XCTAssertEqual(point.y, baselineY - markerFont.ascender, accuracy: 0.1)
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
func testOrderedMarkerDrawingOriginIgnoresTrailingSpaceForHorizontalAlignment() {
|
|
1864
|
-
let markerFont = baseFont
|
|
1865
|
-
let lineFragmentRect = CGRect(x: 24, y: 8, width: 160, height: 32)
|
|
1866
|
-
let usedRect = CGRect(x: 24, y: 14, width: 160, height: 19)
|
|
1867
|
-
let baselineY: CGFloat = 30.140625
|
|
1868
|
-
let markerText = "12. "
|
|
1869
|
-
|
|
1870
|
-
let point = EditorLayoutManager.orderedMarkerDrawingOrigin(
|
|
1871
|
-
usedRect: usedRect,
|
|
1872
|
-
lineFragmentRect: lineFragmentRect,
|
|
1873
|
-
markerWidth: 20,
|
|
1874
|
-
baselineY: baselineY,
|
|
1875
|
-
markerFont: markerFont,
|
|
1876
|
-
markerText: markerText,
|
|
1877
|
-
origin: .zero
|
|
1878
|
-
)
|
|
1879
|
-
let visibleWidth = ceil(("12." as NSString).size(withAttributes: [
|
|
1880
|
-
.font: markerFont,
|
|
1881
|
-
]).width)
|
|
1882
|
-
let fullWidth = ceil((markerText as NSString).size(withAttributes: [
|
|
1883
|
-
.font: markerFont,
|
|
1884
|
-
]).width)
|
|
1885
|
-
|
|
1886
|
-
XCTAssertEqual(point.x, usedRect.minX - 4.0 - visibleWidth, accuracy: 0.1)
|
|
1887
|
-
XCTAssertNotEqual(point.x, usedRect.minX - 4.0 - fullWidth, accuracy: 0.1)
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
func testListMarkerBaseFontUsesParagraphFontInsteadOfLeadingBoldRun() {
|
|
1891
|
-
let json = """
|
|
1892
|
-
[
|
|
1893
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 0,
|
|
1894
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
1895
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 1},
|
|
1896
|
-
{"type": "textRun", "text": "Bold", "marks": ["bold"]},
|
|
1897
|
-
{"type": "textRun", "text": " start", "marks": []},
|
|
1898
|
-
{"type": "blockEnd"},
|
|
1899
|
-
{"type": "blockEnd"}
|
|
1900
|
-
]
|
|
1901
|
-
"""
|
|
1902
|
-
|
|
1903
|
-
let result = RenderBridge.renderElements(
|
|
1904
|
-
fromJSON: json,
|
|
1905
|
-
baseFont: baseFont,
|
|
1906
|
-
textColor: textColor
|
|
1907
|
-
)
|
|
1908
|
-
|
|
1909
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
1910
|
-
let textFont = attrs[.font] as? UIFont
|
|
1911
|
-
let markerBaseFont = attrs[RenderBridgeAttributes.listMarkerBaseFont] as? UIFont
|
|
1912
|
-
|
|
1913
|
-
XCTAssertTrue(
|
|
1914
|
-
textFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
1915
|
-
"First text run should still be bold"
|
|
1916
|
-
)
|
|
1917
|
-
XCTAssertNotNil(markerBaseFont, "List marker should carry its paragraph base font")
|
|
1918
|
-
XCTAssertFalse(
|
|
1919
|
-
markerBaseFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false,
|
|
1920
|
-
"Marker base font should ignore inline bold marks on the first run"
|
|
1921
|
-
)
|
|
1922
|
-
XCTAssertEqual(markerBaseFont?.pointSize ?? 0, baseFont.pointSize, accuracy: 0.1)
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
func testListMarkerParagraphStylePreservesThemedLineHeight() {
|
|
1926
|
-
let sourceStyle = NSMutableParagraphStyle()
|
|
1927
|
-
sourceStyle.minimumLineHeight = 28
|
|
1928
|
-
sourceStyle.maximumLineHeight = 28
|
|
1929
|
-
|
|
1930
|
-
let markerStyle = EditorLayoutManager.markerParagraphStyle(from: [
|
|
1931
|
-
.paragraphStyle: sourceStyle,
|
|
1932
|
-
])
|
|
1933
|
-
|
|
1934
|
-
XCTAssertEqual(markerStyle.minimumLineHeight, 28, accuracy: 0.1)
|
|
1935
|
-
XCTAssertEqual(markerStyle.maximumLineHeight, 28, accuracy: 0.1)
|
|
1936
|
-
XCTAssertEqual(markerStyle.alignment, .right)
|
|
1937
|
-
XCTAssertEqual(markerStyle.lineBreakMode, .byClipping)
|
|
1938
|
-
XCTAssertEqual(markerStyle.firstLineHeadIndent, 0, accuracy: 0.1)
|
|
1939
|
-
XCTAssertEqual(markerStyle.headIndent, 0, accuracy: 0.1)
|
|
1940
|
-
XCTAssertEqual(markerStyle.tailIndent, 0, accuracy: 0.1)
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
func testUnorderedBulletDrawingRectCentersBulletOnTextMidline() {
|
|
1944
|
-
let rect = EditorLayoutManager.unorderedBulletDrawingRect(
|
|
1945
|
-
usedRect: CGRect(x: 24, y: 14, width: 160, height: 19),
|
|
1946
|
-
lineFragmentRect: CGRect(x: 24, y: 8, width: 160, height: 32),
|
|
1947
|
-
markerWidth: 20,
|
|
1948
|
-
baselineY: 28.140625,
|
|
1949
|
-
baseFont: baseFont,
|
|
1950
|
-
markerScale: 2,
|
|
1951
|
-
origin: .zero
|
|
1952
|
-
)
|
|
1953
|
-
let targetMidline = 28.140625 - ((baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0)
|
|
1954
|
-
|
|
1955
|
-
XCTAssertEqual(rect.midY, targetMidline, accuracy: 0.1)
|
|
1956
|
-
XCTAssertGreaterThan(rect.width, 0)
|
|
1957
|
-
XCTAssertGreaterThan(rect.height, 0)
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
func testUnorderedBulletDrawingRectPreservesTextSideGapAcrossMarkerScales() {
|
|
1961
|
-
let usedRect = CGRect(x: 24, y: 14, width: 160, height: 19)
|
|
1962
|
-
let lineFragmentRect = CGRect(x: 24, y: 8, width: 160, height: 32)
|
|
1963
|
-
let baselineY: CGFloat = 28.140625
|
|
1964
|
-
|
|
1965
|
-
let normalRect = EditorLayoutManager.unorderedBulletDrawingRect(
|
|
1966
|
-
usedRect: usedRect,
|
|
1967
|
-
lineFragmentRect: lineFragmentRect,
|
|
1968
|
-
markerWidth: 20,
|
|
1969
|
-
baselineY: baselineY,
|
|
1970
|
-
baseFont: baseFont,
|
|
1971
|
-
markerScale: 1,
|
|
1972
|
-
origin: .zero
|
|
1973
|
-
)
|
|
1974
|
-
let scaledRect = EditorLayoutManager.unorderedBulletDrawingRect(
|
|
1975
|
-
usedRect: usedRect,
|
|
1976
|
-
lineFragmentRect: lineFragmentRect,
|
|
1977
|
-
markerWidth: 20,
|
|
1978
|
-
baselineY: baselineY,
|
|
1979
|
-
baseFont: baseFont,
|
|
1980
|
-
markerScale: 2,
|
|
1981
|
-
origin: .zero
|
|
1982
|
-
)
|
|
1983
|
-
|
|
1984
|
-
XCTAssertEqual(usedRect.minX - normalRect.maxX, usedRect.minX - scaledRect.maxX, accuracy: 0.1)
|
|
1985
|
-
XCTAssertEqual(usedRect.minX - scaledRect.maxX, LayoutConstants.listMarkerTextGap, accuracy: 0.1)
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
func testUnorderedBulletDrawingRectReproducesTallLineHeightListItem() {
|
|
1989
|
-
let theme = EditorTheme(dictionary: [
|
|
1990
|
-
"paragraph": [
|
|
1991
|
-
"lineHeight": 32,
|
|
1992
|
-
],
|
|
1993
|
-
"list": [
|
|
1994
|
-
"markerScale": 2,
|
|
1995
|
-
],
|
|
1996
|
-
])
|
|
1997
|
-
let json = """
|
|
1998
|
-
[
|
|
1999
|
-
{"type": "blockStart", "nodeType": "listItem", "depth": 1,
|
|
2000
|
-
"listContext": {"ordered": false, "index": 1, "total": 1, "start": 1, "isFirst": true, "isLast": true}},
|
|
2001
|
-
{"type": "blockStart", "nodeType": "paragraph", "depth": 2},
|
|
2002
|
-
{"type": "textRun", "text": "Bullet item", "marks": []},
|
|
2003
|
-
{"type": "blockEnd"},
|
|
2004
|
-
{"type": "blockEnd"}
|
|
2005
|
-
]
|
|
2006
|
-
"""
|
|
2007
|
-
let result = RenderBridge.renderElements(
|
|
2008
|
-
fromJSON: json,
|
|
2009
|
-
baseFont: baseFont,
|
|
2010
|
-
textColor: textColor,
|
|
2011
|
-
theme: theme
|
|
2012
|
-
)
|
|
2013
|
-
|
|
2014
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
2015
|
-
let textFont = attrs[.font] as? UIFont ?? baseFont
|
|
2016
|
-
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
2017
|
-
let markerScale = (attrs[RenderBridgeAttributes.listMarkerScale] as? NSNumber)
|
|
2018
|
-
.map { CGFloat(truncating: $0) }
|
|
2019
|
-
?? 1
|
|
2020
|
-
let bulletRect = EditorLayoutManager.unorderedBulletDrawingRect(
|
|
2021
|
-
usedRect: CGRect(x: 24, y: 14, width: 160, height: 19),
|
|
2022
|
-
lineFragmentRect: CGRect(x: 24, y: 8, width: 160, height: 32),
|
|
2023
|
-
markerWidth: 20,
|
|
2024
|
-
baselineY: 28.140625,
|
|
2025
|
-
baseFont: textFont,
|
|
2026
|
-
markerScale: markerScale,
|
|
2027
|
-
origin: .zero
|
|
2028
|
-
)
|
|
2029
|
-
let expectedCenterY = 28.140625 - ((textFont.xHeight > 0 ? textFont.xHeight : textFont.capHeight) / 2.0)
|
|
2030
|
-
|
|
2031
|
-
XCTAssertNotNil(attrs[RenderBridgeAttributes.listMarkerContext])
|
|
2032
|
-
XCTAssertEqual(paragraphStyle?.minimumLineHeight ?? 0, 32, accuracy: 0.1)
|
|
2033
|
-
XCTAssertEqual(paragraphStyle?.maximumLineHeight ?? 0, 32, accuracy: 0.1)
|
|
2034
|
-
XCTAssertEqual(bulletRect.midY, expectedCenterY, accuracy: 0.1)
|
|
2035
|
-
XCTAssertGreaterThan(bulletRect.width, 0)
|
|
2036
|
-
XCTAssertGreaterThan(bulletRect.height, 0)
|
|
2037
|
-
XCTAssertEqual(bulletRect.width, bulletRect.height, accuracy: 0.1)
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
func testOrderedListMarkerBaselineOffsetIsNeutral() {
|
|
2041
|
-
let orderedContext: [String: Any] = ["ordered": true]
|
|
2042
|
-
|
|
2043
|
-
let offset = EditorLayoutManager.markerBaselineOffset(
|
|
2044
|
-
for: orderedContext,
|
|
2045
|
-
baseFont: baseFont,
|
|
2046
|
-
markerFont: baseFont
|
|
2047
|
-
)
|
|
2048
|
-
|
|
2049
|
-
XCTAssertEqual(offset, 0, accuracy: 0.1)
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
func testMarkerBaseFontPrefersStoredParagraphFont() {
|
|
2053
|
-
let boldDescriptor = baseFont.fontDescriptor.withSymbolicTraits(.traitBold)
|
|
2054
|
-
?? baseFont.fontDescriptor
|
|
2055
|
-
let boldFont = UIFont(descriptor: boldDescriptor, size: baseFont.pointSize)
|
|
2056
|
-
let resolved = EditorLayoutManager.markerBaseFont(from: [
|
|
2057
|
-
.font: boldFont,
|
|
2058
|
-
RenderBridgeAttributes.listMarkerBaseFont: baseFont,
|
|
2059
|
-
])
|
|
2060
|
-
|
|
2061
|
-
XCTAssertFalse(
|
|
2062
|
-
resolved.fontDescriptor.symbolicTraits.contains(.traitBold),
|
|
2063
|
-
"Stored paragraph font should win over the inline bold run font"
|
|
2064
|
-
)
|
|
2065
|
-
XCTAssertEqual(resolved.pointSize, baseFont.pointSize, accuracy: 0.1)
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// MARK: - HorizontalRuleAttachment
|
|
2069
|
-
|
|
2070
|
-
func testHorizontalRuleAttachment_bounds() {
|
|
2071
|
-
let attachment = HorizontalRuleAttachment()
|
|
2072
|
-
let proposedRect = CGRect(x: 0, y: 0, width: 320, height: 20)
|
|
2073
|
-
let bounds = attachment.attachmentBounds(
|
|
2074
|
-
for: nil,
|
|
2075
|
-
proposedLineFragment: proposedRect,
|
|
2076
|
-
glyphPosition: .zero,
|
|
2077
|
-
characterIndex: 0
|
|
2078
|
-
)
|
|
2079
|
-
|
|
2080
|
-
XCTAssertEqual(
|
|
2081
|
-
bounds.width, 320,
|
|
2082
|
-
"Attachment width should match proposed line fragment width"
|
|
2083
|
-
)
|
|
2084
|
-
let expectedHeight = 1.0 + (8.0 * 2) // line + padding
|
|
2085
|
-
XCTAssertEqual(
|
|
2086
|
-
bounds.height, expectedHeight,
|
|
2087
|
-
"Attachment height should be line height + 2 * vertical padding"
|
|
2088
|
-
)
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
func testHorizontalRuleAttachment_rendersImage() {
|
|
2092
|
-
let attachment = HorizontalRuleAttachment()
|
|
2093
|
-
attachment.lineColor = .red
|
|
2094
|
-
let bounds = CGRect(x: 0, y: 0, width: 200, height: 17)
|
|
2095
|
-
let image = attachment.image(
|
|
2096
|
-
forBounds: bounds,
|
|
2097
|
-
textContainer: nil,
|
|
2098
|
-
characterIndex: 0
|
|
2099
|
-
)
|
|
2100
|
-
XCTAssertNotNil(image, "HorizontalRuleAttachment should produce a non-nil image")
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// MARK: - Height Measurement
|
|
2104
|
-
|
|
2105
|
-
func testMeasureHeightForSingleParagraph() {
|
|
2106
|
-
let renderJSON = """
|
|
2107
|
-
[
|
|
2108
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
2109
|
-
{"type":"textRun","text":"Hello world"},
|
|
2110
|
-
{"type":"blockEnd"}
|
|
2111
|
-
]
|
|
2112
|
-
"""
|
|
2113
|
-
let height = RenderBridge.measureHeight(
|
|
2114
|
-
forRenderJSON: renderJSON,
|
|
2115
|
-
themeJSON: nil,
|
|
2116
|
-
width: 375
|
|
2117
|
-
)
|
|
2118
|
-
XCTAssertGreaterThan(height, 0, "Single paragraph should have positive height")
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
func testMeasureHeightForEmptyContent() {
|
|
2122
|
-
let renderJSON = "[]"
|
|
2123
|
-
let height = RenderBridge.measureHeight(
|
|
2124
|
-
forRenderJSON: renderJSON,
|
|
2125
|
-
themeJSON: nil,
|
|
2126
|
-
width: 375
|
|
2127
|
-
)
|
|
2128
|
-
XCTAssertEqual(height, 0, "Empty content should have zero height")
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
func testMeasureHeightRespectsWidth() {
|
|
2132
|
-
let longText = String(repeating: "word ", count: 100)
|
|
2133
|
-
let renderJSON = """
|
|
2134
|
-
[
|
|
2135
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
2136
|
-
{"type":"textRun","text":"\(longText)"},
|
|
2137
|
-
{"type":"blockEnd"}
|
|
2138
|
-
]
|
|
2139
|
-
"""
|
|
2140
|
-
let narrowHeight = RenderBridge.measureHeight(
|
|
2141
|
-
forRenderJSON: renderJSON,
|
|
2142
|
-
themeJSON: nil,
|
|
2143
|
-
width: 100
|
|
2144
|
-
)
|
|
2145
|
-
let wideHeight = RenderBridge.measureHeight(
|
|
2146
|
-
forRenderJSON: renderJSON,
|
|
2147
|
-
themeJSON: nil,
|
|
2148
|
-
width: 1000
|
|
2149
|
-
)
|
|
2150
|
-
XCTAssertGreaterThan(narrowHeight, wideHeight, "Narrower width should produce taller height")
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
func testMeasureHeightRespectsThemeFontSize() {
|
|
2154
|
-
let renderJSON = """
|
|
2155
|
-
[
|
|
2156
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
2157
|
-
{"type":"textRun","text":"Hello world"},
|
|
2158
|
-
{"type":"blockEnd"}
|
|
2159
|
-
]
|
|
2160
|
-
"""
|
|
2161
|
-
let smallTheme = """
|
|
2162
|
-
{"text":{"fontSize":12}}
|
|
2163
|
-
"""
|
|
2164
|
-
let largeTheme = """
|
|
2165
|
-
{"text":{"fontSize":32}}
|
|
2166
|
-
"""
|
|
2167
|
-
let smallHeight = RenderBridge.measureHeight(
|
|
2168
|
-
forRenderJSON: renderJSON,
|
|
2169
|
-
themeJSON: smallTheme,
|
|
2170
|
-
width: 375
|
|
2171
|
-
)
|
|
2172
|
-
let largeHeight = RenderBridge.measureHeight(
|
|
2173
|
-
forRenderJSON: renderJSON,
|
|
2174
|
-
themeJSON: largeTheme,
|
|
2175
|
-
width: 375
|
|
2176
|
-
)
|
|
2177
|
-
XCTAssertGreaterThan(largeHeight, smallHeight, "Larger font should produce taller height")
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
func testMeasureHeightRespectsContentInsets() {
|
|
2181
|
-
let renderJSON = """
|
|
2182
|
-
[
|
|
2183
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
2184
|
-
{"type":"textRun","text":"Hello world"},
|
|
2185
|
-
{"type":"blockEnd"}
|
|
2186
|
-
]
|
|
2187
|
-
"""
|
|
2188
|
-
let noInsetHeight = RenderBridge.measureHeight(
|
|
2189
|
-
forRenderJSON: renderJSON,
|
|
2190
|
-
themeJSON: nil,
|
|
2191
|
-
width: 375
|
|
2192
|
-
)
|
|
2193
|
-
let insetTheme = """
|
|
2194
|
-
{"contentInsets":{"top":20,"bottom":20}}
|
|
2195
|
-
"""
|
|
2196
|
-
let insetHeight = RenderBridge.measureHeight(
|
|
2197
|
-
forRenderJSON: renderJSON,
|
|
2198
|
-
themeJSON: insetTheme,
|
|
2199
|
-
width: 375
|
|
2200
|
-
)
|
|
2201
|
-
XCTAssertEqual(insetHeight, noInsetHeight + 40, accuracy: 1.0, "Content insets should add to height")
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
func testRender_imageAttachmentHonorsPreferredDimensions() {
|
|
2205
|
-
let json = """
|
|
2206
|
-
[
|
|
2207
|
-
{"type": "voidBlock", "nodeType": "image", "docPos": 1, "attrs": {
|
|
2208
|
-
"src": "https://example.com/cat.png",
|
|
2209
|
-
"width": 140,
|
|
2210
|
-
"height": 80
|
|
2211
|
-
}}
|
|
2212
|
-
]
|
|
2213
|
-
"""
|
|
2214
|
-
let result = RenderBridge.renderElements(
|
|
2215
|
-
fromJSON: json,
|
|
2216
|
-
baseFont: baseFont,
|
|
2217
|
-
textColor: textColor
|
|
2218
|
-
)
|
|
2219
|
-
|
|
2220
|
-
XCTAssertEqual(result.string, LayoutConstants.objectReplacementCharacter)
|
|
2221
|
-
|
|
2222
|
-
let attrs = result.attributes(at: 0, effectiveRange: nil)
|
|
2223
|
-
let attachment = attrs[.attachment] as? NSTextAttachment
|
|
2224
|
-
XCTAssertNotNil(attachment, "Image render should produce an attachment")
|
|
2225
|
-
|
|
2226
|
-
let bounds = attachment?.attachmentBounds(
|
|
2227
|
-
for: nil,
|
|
2228
|
-
proposedLineFragment: CGRect(x: 0, y: 0, width: 320, height: 24),
|
|
2229
|
-
glyphPosition: .zero,
|
|
2230
|
-
characterIndex: 0
|
|
2231
|
-
)
|
|
2232
|
-
|
|
2233
|
-
XCTAssertEqual(bounds?.width ?? 0, 140, accuracy: 0.1)
|
|
2234
|
-
XCTAssertEqual(bounds?.height ?? 0, 80, accuracy: 0.1)
|
|
2235
|
-
}
|
|
2236
|
-
}
|