@apollohg/react-native-rich-text-editor 2.0.3 → 2.0.5
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/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +8 -3
- package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +5 -1
- package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +3 -2
- package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +196 -8
- package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +7 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +48 -9
- package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +17 -4
- package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +83 -25
- package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +28 -14
- package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +5 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +5 -1
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +141 -40
- package/dist/EditorStyleSheetNormalization.js +37 -0
- package/dist/EditorStyleSheetTypes.d.ts +22 -1
- package/dist/EditorTheme.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/useRichTextEditorCommands.js +1 -6
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +2 -4
- package/ios/EditorStyleSheet.swift +77 -11
- package/ios/EditorTextView+Commands.swift +1 -1
- package/ios/EditorTextView+Selection.swift +5 -2
- package/ios/EditorTextView.swift +5 -2
- package/ios/EditorTheme.swift +3 -1
- package/ios/EditorV2Adapter+Commands.swift +10 -0
- package/ios/EditorV2Shadow.swift +4 -0
- package/ios/RenderBridge+Atoms.swift +15 -6
- package/ios/RenderBridge+Style.swift +59 -17
- package/ios/RenderBridge.swift +19 -16
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +31 -13
- package/ios/Viewer/CoreTextProseLayoutEngine+Models.swift +17 -2
- package/ios/Viewer/CoreTextProseLayoutEngine.swift +39 -21
- package/package.json +3 -3
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/EditorStyleSheetNormalization.ts +50 -0
- package/src/EditorStyleSheetTypes.ts +51 -6
- package/src/EditorTheme.ts +5 -1
- package/src/index.ts +1 -0
- package/src/useRichTextEditorCommands.tsx +1 -7
|
@@ -2,6 +2,34 @@ import UIKit
|
|
|
2
2
|
|
|
3
3
|
struct EditorStyleSheet {
|
|
4
4
|
let styles: [String: [String: Any]]
|
|
5
|
+
let rules: [Rule]
|
|
6
|
+
|
|
7
|
+
struct Rule {
|
|
8
|
+
let path: [String]
|
|
9
|
+
let style: [String: Any]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
init(styles: [String: [String: Any]], rules: [Rule] = []) {
|
|
13
|
+
self.styles = styles
|
|
14
|
+
self.rules = rules
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static func decodeRules(_ value: Any?) -> [Rule] {
|
|
18
|
+
let names: Set<String> = [
|
|
19
|
+
"content", "text", "paragraph", "h1", "h2", "h3", "h4", "h5", "h6",
|
|
20
|
+
"blockquote", "codeBlock", "bulletList", "orderedList", "taskList", "listItem",
|
|
21
|
+
"taskItem", "listMarker", "taskCheckbox", "horizontalRule", "image", "link",
|
|
22
|
+
"inlineCode", "bold", "italic", "underline", "strike", "mention", "placeholder"
|
|
23
|
+
]
|
|
24
|
+
return (value as? [Any] ?? []).compactMap { entry in
|
|
25
|
+
guard let entry = entry as? [String: Any],
|
|
26
|
+
let rawPath = entry["path"] as? [String], !rawPath.isEmpty,
|
|
27
|
+
let style = entry["style"] as? [String: Any] else { return nil }
|
|
28
|
+
let path = rawPath.map(Self.element)
|
|
29
|
+
guard path.allSatisfy({ names.contains($0) }) else { return nil }
|
|
30
|
+
return Rule(path: path, style: style)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
5
33
|
|
|
6
34
|
static func collapsedMargin(_ first: CGFloat, _ second: CGFloat) -> CGFloat {
|
|
7
35
|
max(0, first, second) + min(0, first, second)
|
|
@@ -26,7 +54,38 @@ struct EditorStyleSheet {
|
|
|
26
54
|
|
|
27
55
|
subscript(_ element: String) -> [String: Any] { styles[Self.element(element)] ?? [:] }
|
|
28
56
|
|
|
57
|
+
private func matchingRules(_ element: String, ancestors: [String]) -> [Rule] {
|
|
58
|
+
let chain = (ancestors + [element]).map(Self.element)
|
|
59
|
+
return rules.filter { $0.path.count <= chain.count && Array(chain.suffix($0.path.count)) == $0.path }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func hasInlineFontRule(_ marks: [Any], ancestors: [String]) -> Bool {
|
|
63
|
+
marks.contains { mark in
|
|
64
|
+
guard let rawName = (mark as? String) ?? (mark as? [String: Any])?["type"] as? String else { return false }
|
|
65
|
+
let name = Self.element(rawName)
|
|
66
|
+
return Self.inlineMarkNames.contains(name)
|
|
67
|
+
&& matchingRules(name, ancestors: ancestors).contains { $0.style["fontFamily"] is String }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func resolvedValues(_ element: String, ancestors: [String] = []) -> [String: Any] {
|
|
72
|
+
var values = self[element]
|
|
73
|
+
for rule in matchingRules(element, ancestors: ancestors) {
|
|
74
|
+
values.merge(rule.style, uniquingKeysWith: Self.mergeValue)
|
|
75
|
+
}
|
|
76
|
+
return values
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private static func mergeValue(_ current: Any, _ next: Any) -> Any {
|
|
80
|
+
guard let current = current as? [String: Any], let next = next as? [String: Any] else { return next }
|
|
81
|
+
return current.merging(next, uniquingKeysWith: Self.mergeValue)
|
|
82
|
+
}
|
|
83
|
+
|
|
29
84
|
func box(_ element: String) -> EditorStyleBox {
|
|
85
|
+
box(element, ancestors: [])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func box(_ element: String, ancestors: [String]) -> EditorStyleBox {
|
|
30
89
|
var values: [String: Any] = [:]
|
|
31
90
|
switch Self.element(element) {
|
|
32
91
|
case "codeBlock":
|
|
@@ -40,7 +99,7 @@ struct EditorStyleSheet {
|
|
|
40
99
|
case "horizontalRule": values = ["backgroundColor": UIColor.separator, "marginTop": 12, "marginBottom": 12]
|
|
41
100
|
default: break
|
|
42
101
|
}
|
|
43
|
-
values.merge(
|
|
102
|
+
values.merge(resolvedValues(element, ancestors: ancestors)) { _, new in new }
|
|
44
103
|
return EditorStyleBox(values)
|
|
45
104
|
}
|
|
46
105
|
|
|
@@ -53,19 +112,23 @@ struct EditorStyleSheet {
|
|
|
53
112
|
if name == "codeBlock" { style = style.merged(with: EditorTextStyle(fontFamily: "monospace")) }
|
|
54
113
|
style = style.merged(with: semantic)
|
|
55
114
|
for ancestor in ancestors { style = style.merged(with: EditorTextStyle(dictionary: self[ancestor])) }
|
|
56
|
-
return style.merged(with: EditorTextStyle(dictionary:
|
|
115
|
+
return style.merged(with: EditorTextStyle(dictionary: resolvedValues(name, ancestors: ancestors)))
|
|
57
116
|
}
|
|
58
117
|
|
|
59
118
|
func textValues(_ element: String, ancestors: [String] = []) -> [String: Any] {
|
|
60
119
|
let keys = ["letterSpacing", "textAlign", "textDecorationLine", "textDecorationColor", "textDecorationStyle"]
|
|
61
120
|
var result: [String: Any] = [:]
|
|
62
|
-
for layer in ["text"] + ancestors
|
|
121
|
+
for layer in ["text"] + ancestors {
|
|
63
122
|
for key in keys where self[layer][key] != nil { result[key] = self[layer][key] }
|
|
64
123
|
}
|
|
124
|
+
let target = resolvedValues(element, ancestors: ancestors)
|
|
125
|
+
for key in keys where target[key] != nil { result[key] = target[key] }
|
|
65
126
|
return result
|
|
66
127
|
}
|
|
67
128
|
|
|
68
|
-
|
|
129
|
+
private static let inlineMarkNames = ["inlineCode", "bold", "italic", "link", "underline", "strike"]
|
|
130
|
+
|
|
131
|
+
func inlineAttributes(_ marks: [Any], base: [NSAttributedString.Key: Any], scale: CGFloat = 1, ancestors: [String] = []) -> [NSAttributedString.Key: Any] {
|
|
69
132
|
var attributes = base
|
|
70
133
|
var byName: [String: [String: Any]] = [:]
|
|
71
134
|
for mark in marks {
|
|
@@ -74,7 +137,7 @@ struct EditorStyleSheet {
|
|
|
74
137
|
byName[Self.element(name)] = object
|
|
75
138
|
}
|
|
76
139
|
}
|
|
77
|
-
for name in
|
|
140
|
+
for name in Self.inlineMarkNames where byName[name] != nil {
|
|
78
141
|
var values: [String: Any] = [:]
|
|
79
142
|
switch name {
|
|
80
143
|
case "inlineCode": values = ["fontFamily": "monospace"]
|
|
@@ -85,7 +148,7 @@ struct EditorStyleSheet {
|
|
|
85
148
|
case "strike": values = ["textDecorationLine": "line-through"]
|
|
86
149
|
default: break
|
|
87
150
|
}
|
|
88
|
-
values.merge(
|
|
151
|
+
values.merge(resolvedValues(name, ancestors: ancestors)) { _, new in new }
|
|
89
152
|
Self.applyText(values, to: &attributes, scale: scale)
|
|
90
153
|
if name == "link", let href = byName[name]?["href"] as? String {
|
|
91
154
|
attributes[RenderBridgeAttributes.linkHref] = href
|
|
@@ -334,8 +397,10 @@ extension RenderBridge {
|
|
|
334
397
|
}
|
|
335
398
|
guard start < result.length else { return }
|
|
336
399
|
let range = NSRange(location: start, length: result.length - start)
|
|
337
|
-
let outer = ancestors.reduce(UIEdgeInsets.zero) {
|
|
338
|
-
|
|
400
|
+
let outer = ancestors.enumerated().reduce(UIEdgeInsets.zero) { total, entry in
|
|
401
|
+
total.adding(sheet.box(entry.element.nodeType, ancestors: ancestors.prefix(entry.offset).map(\.nodeType)).outerInsets)
|
|
402
|
+
}
|
|
403
|
+
var values = sheet.box(context.nodeType, ancestors: ancestors.map(\.nodeType)).values
|
|
339
404
|
if omitBottomMargin { values["marginBottom"] = 0 }
|
|
340
405
|
let box = EditorStyleBox(values)
|
|
341
406
|
let descriptor = EditorRenderedBox(box: box, depth: ancestors.count, leading: outer.left + box.margin.left, trailing: outer.right + box.margin.right)
|
|
@@ -444,10 +509,11 @@ final class EditorStyleBoxView: UIView {
|
|
|
444
509
|
let editorTaskCheckboxAttribute = NSAttributedString.Key("com.apollohg.editor.taskCheckbox")
|
|
445
510
|
|
|
446
511
|
extension EditorStyleSheet {
|
|
447
|
-
func checkbox(checked: Bool) -> EditorStyleBox {
|
|
512
|
+
func checkbox(checked: Bool, ancestors: [String] = []) -> EditorStyleBox {
|
|
448
513
|
var values: [String: Any] = ["borderWidth": 1.8, "borderColor": "#8e8e93ff", "borderRadius": 5, "size": 24, "gap": 8, "checkColor": "#007affff"]
|
|
449
|
-
|
|
450
|
-
|
|
514
|
+
let resolved = resolvedValues("taskCheckbox", ancestors: ancestors)
|
|
515
|
+
values.merge(resolved) { _, new in new }
|
|
516
|
+
if checked { values.merge(resolved["checked"] as? [String: Any] ?? [:]) { _, new in new } }
|
|
451
517
|
return EditorStyleBox(values)
|
|
452
518
|
}
|
|
453
519
|
|
|
@@ -26,7 +26,7 @@ extension EditorTextView {
|
|
|
26
26
|
scalarAnchor: selection.anchor,
|
|
27
27
|
scalarHead: selection.head
|
|
28
28
|
)
|
|
29
|
-
: EditorV2Shadow.
|
|
29
|
+
: EditorV2Shadow.applyListTypeAtSelectionScalar(
|
|
30
30
|
id: editorId,
|
|
31
31
|
scalarAnchor: selection.anchor,
|
|
32
32
|
scalarHead: selection.head,
|
|
@@ -49,7 +49,8 @@ extension EditorTextView {
|
|
|
49
49
|
prepareForExternalEditorUpdate(),
|
|
50
50
|
let position = closestPosition(to: point)
|
|
51
51
|
else { return false }
|
|
52
|
-
let
|
|
52
|
+
let caretPosition = autocapitalizationFriendlyEmptyBlockPosition(for: position) ?? position
|
|
53
|
+
let offset = self.offset(from: beginningOfDocument, to: caretPosition)
|
|
53
54
|
guard !isAtomBoundaryCaretOffset(offset) else { return false }
|
|
54
55
|
_ = becomeFirstResponder()
|
|
55
56
|
logicalSelectionScalarRange = nil
|
|
@@ -154,7 +155,9 @@ extension EditorTextView {
|
|
|
154
155
|
selectedRange.length == 0,
|
|
155
156
|
let point = caretPlacementTapRecognizer.pendingCaretPoint,
|
|
156
157
|
let position = closestPosition(to: point) {
|
|
157
|
-
|
|
158
|
+
// Match empty-block normalization so synchronous callbacks cannot alternate offsets.
|
|
159
|
+
let caretPosition = autocapitalizationFriendlyEmptyBlockPosition(for: position) ?? position
|
|
160
|
+
let offset = self.offset(from: beginningOfDocument, to: caretPosition)
|
|
158
161
|
if selectedRange.location != offset, !isAtomBoundaryCaretOffset(offset) {
|
|
159
162
|
// Correct UIKit's word snap before publishing or drawing the selection.
|
|
160
163
|
selectedRange = NSRange(location: offset, length: 0)
|
package/ios/EditorTextView.swift
CHANGED
|
@@ -97,12 +97,15 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
97
97
|
placeholderLabel.textColor = theme?.placeholderColor ?? .placeholderText
|
|
98
98
|
if let sheet = theme?.styleSheet {
|
|
99
99
|
var attributes: [NSAttributedString.Key: Any] = [.font: resolvedDefaultFont(), .foregroundColor: theme?.placeholderColor ?? UIColor.placeholderText]
|
|
100
|
-
EditorStyleSheet.applyText(sheet
|
|
100
|
+
EditorStyleSheet.applyText(sheet.resolvedValues("placeholder", ancestors: []), to: &attributes)
|
|
101
101
|
placeholderLabel.attributedText = NSAttributedString(string: placeholder, attributes: attributes)
|
|
102
102
|
} else { placeholderLabel.attributedText = nil; placeholderLabel.text = placeholder }
|
|
103
103
|
styleContentView.box = theme?.styleSheet?.box("content")
|
|
104
104
|
backgroundColor = theme?.backgroundColor ?? baseBackgroundColor
|
|
105
|
-
if let
|
|
105
|
+
if let box = theme?.styleSheet?.box("content", ancestors: []) {
|
|
106
|
+
textContainerInset = box.inset
|
|
107
|
+
textContainer.lineFragmentPadding = 0
|
|
108
|
+
} else if let contentInsets = theme?.contentInsets {
|
|
106
109
|
textContainerInset = UIEdgeInsets(
|
|
107
110
|
top: contentInsets.top ?? 0,
|
|
108
111
|
left: contentInsets.left ?? 0,
|
package/ios/EditorTheme.swift
CHANGED
|
@@ -398,6 +398,7 @@ struct EditorContentInsets {
|
|
|
398
398
|
|
|
399
399
|
struct EditorTheme {
|
|
400
400
|
var styleSheet: EditorStyleSheet?
|
|
401
|
+
var styleSheetMentionOverrides: [String: Any]?
|
|
401
402
|
var text: EditorTextStyle?
|
|
402
403
|
var paragraph: EditorTextStyle?
|
|
403
404
|
var blockquote: EditorBlockquoteTheme?
|
|
@@ -432,7 +433,8 @@ struct EditorTheme {
|
|
|
432
433
|
dictionary["styles"] == nil || dictionary["styles"] is [String: [String: Any]] {
|
|
433
434
|
let styles = dictionary["styles"] as? [String: [String: Any]] ?? [:]
|
|
434
435
|
self.init(legacyDictionary: Self.legacyProjection(styles: styles, root: dictionary))
|
|
435
|
-
styleSheet = EditorStyleSheet(styles: styles)
|
|
436
|
+
styleSheet = EditorStyleSheet(styles: styles, rules: EditorStyleSheet.decodeRules(dictionary["rules"]))
|
|
437
|
+
styleSheetMentionOverrides = dictionary["mentions"] as? [String: Any]
|
|
436
438
|
return
|
|
437
439
|
}
|
|
438
440
|
self.init(legacyDictionary: dictionary)
|
|
@@ -69,6 +69,16 @@ extension EditorV2Adapter {
|
|
|
69
69
|
return commandAtSelection(["type": "toggleBlockquote"], anchor: anchor, head: head)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
func applyListType(_ listType: String, anchor: UInt32, head: UInt32) -> String? {
|
|
73
|
+
guard beginRuntimeOperation() else { return nil }
|
|
74
|
+
defer { endRuntimeOperation() }
|
|
75
|
+
return commandAtSelection(
|
|
76
|
+
["type": "applyListType", "listType": listType],
|
|
77
|
+
anchor: anchor,
|
|
78
|
+
head: head
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
func wrapInList(listType: String, itemType: String, anchor: UInt32, head: UInt32) -> String? {
|
|
73
83
|
guard beginRuntimeOperation() else { return nil }
|
|
74
84
|
defer { endRuntimeOperation() }
|
package/ios/EditorV2Shadow.swift
CHANGED
|
@@ -200,6 +200,10 @@ enum EditorV2Shadow {
|
|
|
200
200
|
return adapter(for: id)?.wrapInList(listType: listType, itemType: itemType, anchor: scalarAnchor, head: scalarHead) ?? "{}"
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
static func applyListTypeAtSelectionScalar(id: UInt64, scalarAnchor: UInt32, scalarHead: UInt32, listType: String) -> String {
|
|
204
|
+
adapter(for: id)?.applyListType(listType, anchor: scalarAnchor, head: scalarHead) ?? "{}"
|
|
205
|
+
}
|
|
206
|
+
|
|
203
207
|
static func unwrapFromListAtSelectionScalar(id: UInt64, scalarAnchor: UInt32, scalarHead: UInt32) -> String {
|
|
204
208
|
adapter(for: id)?.unwrapFromList(anchor: scalarAnchor, head: scalarHead) ?? "{}"
|
|
205
209
|
}
|
|
@@ -144,7 +144,8 @@ extension RenderBridge {
|
|
|
144
144
|
topLevelChildIndex: Int?,
|
|
145
145
|
theme: EditorTheme?,
|
|
146
146
|
atomKey: String,
|
|
147
|
-
atomConfiguration: AtomRenderConfiguration
|
|
147
|
+
atomConfiguration: AtomRenderConfiguration?,
|
|
148
|
+
ancestors: [String] = []
|
|
148
149
|
) -> NSAttributedString {
|
|
149
150
|
var attrs = defaultAttributes(baseFont: baseFont, textColor: textColor)
|
|
150
151
|
attrs[RenderBridgeAttributes.voidNodeType] = nodeType
|
|
@@ -171,9 +172,9 @@ extension RenderBridge {
|
|
|
171
172
|
switch nodeType {
|
|
172
173
|
case "horizontalRule", "horizontal_rule":
|
|
173
174
|
let attachment = HorizontalRuleAttachment()
|
|
174
|
-
attachment.styleBox = theme?.styleSheet?.box("horizontalRule")
|
|
175
|
-
attachment.lineColor = theme?.horizontalRule?.color ?? textColor.withAlphaComponent(0.3)
|
|
176
|
-
attachment.lineHeight = theme?.horizontalRule?.thickness ?? LayoutConstants.horizontalRuleHeight
|
|
175
|
+
attachment.styleBox = theme?.styleSheet?.box("horizontalRule", ancestors: ancestors)
|
|
176
|
+
attachment.lineColor = attachment.styleBox?.color("backgroundColor") ?? theme?.horizontalRule?.color ?? textColor.withAlphaComponent(0.3)
|
|
177
|
+
attachment.lineHeight = attachment.styleBox.map { $0.number("height", fallback: 1) } ?? theme?.horizontalRule?.thickness ?? LayoutConstants.horizontalRuleHeight
|
|
177
178
|
attachment.verticalPadding = resolvedHorizontalRuleVerticalMargin(theme: theme)
|
|
178
179
|
let attrStr = NSMutableAttributedString(
|
|
179
180
|
attachment: attachment
|
|
@@ -195,7 +196,7 @@ extension RenderBridge {
|
|
|
195
196
|
preferredWidth: jsonCGFloat(elementAttrs["width"]),
|
|
196
197
|
preferredHeight: jsonCGFloat(elementAttrs["height"])
|
|
197
198
|
)
|
|
198
|
-
attachment.styleBox = theme?.styleSheet?.box("image")
|
|
199
|
+
attachment.styleBox = theme?.styleSheet?.box("image", ancestors: ancestors)
|
|
199
200
|
let attrStr = NSMutableAttributedString(attachment: attachment)
|
|
200
201
|
let range = NSRange(location: 0, length: attrStr.length)
|
|
201
202
|
attrStr.addAttributes(attrs, range: range)
|
|
@@ -226,7 +227,15 @@ extension RenderBridge {
|
|
|
226
227
|
attrs[RenderBridgeAttributes.voidNodeType] = nodeType
|
|
227
228
|
attrs[RenderBridgeAttributes.docPos] = docPos
|
|
228
229
|
if nodeType == "mention" {
|
|
229
|
-
|
|
230
|
+
var globalMentionTheme = theme?.mentions
|
|
231
|
+
if let sheet = theme?.styleSheet, !sheet.rules.isEmpty {
|
|
232
|
+
let projection = EditorTheme.legacyProjection(
|
|
233
|
+
styles: ["mention": sheet.resolvedValues("mention", ancestors: blockStack.map(\.nodeType))],
|
|
234
|
+
root: theme?.styleSheetMentionOverrides.map { ["mentions": $0] } ?? [:]
|
|
235
|
+
)
|
|
236
|
+
globalMentionTheme = (projection["mentions"] as? [String: Any]).map(EditorMentionTheme.init(dictionary:))
|
|
237
|
+
}
|
|
238
|
+
let resolvedMentionTheme = globalMentionTheme?.merged(with: mentionTheme) ?? mentionTheme
|
|
230
239
|
let node = resolvedMentionTheme?.node
|
|
231
240
|
attrs[.foregroundColor] = node?.textColor ?? blockColor
|
|
232
241
|
attrs[.backgroundColor] =
|
|
@@ -17,13 +17,38 @@ extension RenderBridge {
|
|
|
17
17
|
if let sheet = theme?.styleSheet {
|
|
18
18
|
let ancestors = blockStack.dropLast().map(\.nodeType)
|
|
19
19
|
let text = sheet.textStyle(context.nodeType, ancestors: ancestors)
|
|
20
|
-
let horizontal = blockStack.reduce(UIEdgeInsets.zero) {
|
|
20
|
+
let horizontal = blockStack.enumerated().reduce(UIEdgeInsets.zero) { total, entry in
|
|
21
|
+
total.adding(sheet.box(entry.element.nodeType, ancestors: blockStack.prefix(entry.offset).map(\.nodeType)).outerInsets)
|
|
22
|
+
}
|
|
21
23
|
let listName = context.listContext.map { ($0["kind"] as? String) == "task" ? "taskList" : (($0["ordered"] as? NSNumber)?.boolValue == true ? "orderedList" : "bulletList") }
|
|
24
|
+
let ownerIndex = blockStack.lastIndex { $0.listContext != nil }
|
|
25
|
+
let markerAncestors = ownerIndex.map { blockStack.prefix($0 + 1).map(\.nodeType) } ?? []
|
|
22
26
|
let list = listName.map { sheet[$0] } ?? [:]
|
|
27
|
+
var contextualIndent: CGFloat = 0
|
|
28
|
+
for (listDepth, entry) in blockStack.enumerated().filter({ $0.element.listContext != nil }).enumerated() {
|
|
29
|
+
let context = entry.element.listContext!
|
|
30
|
+
let name = (context["kind"] as? String) == "task" ? "taskList" : ((context["ordered"] as? NSNumber)?.boolValue == true ? "orderedList" : "bulletList")
|
|
31
|
+
let prefix = Array(blockStack.prefix(entry.offset))
|
|
32
|
+
let containerIndex = prefix.lastIndex { EditorStyleSheet.element($0.nodeType) == name } ?? prefix.count
|
|
33
|
+
let resolved = sheet.resolvedValues(name, ancestors: prefix.prefix(containerIndex).map(\.nodeType))
|
|
34
|
+
let base = sheet[name]
|
|
35
|
+
let resolvedIndent = EditorTheme.cgFloat(resolved["indent"]) ?? LayoutConstants.indentPerDepth
|
|
36
|
+
let baseIndent = EditorTheme.cgFloat(base["indent"]) ?? LayoutConstants.indentPerDepth
|
|
37
|
+
let resolvedMultiplier = listDepth == 0 ? EditorTheme.cgFloat(resolved["baseIndentMultiplier"]) ?? 1 : 1
|
|
38
|
+
let baseMultiplier = listDepth == 0 ? EditorTheme.cgFloat(base["baseIndentMultiplier"]) ?? 1 : 1
|
|
39
|
+
contextualIndent += resolvedIndent * resolvedMultiplier - baseIndent * baseMultiplier
|
|
40
|
+
}
|
|
23
41
|
let indent = EditorTheme.cgFloat(list["indent"]) ?? LayoutConstants.indentPerDepth
|
|
24
42
|
let multiplier = EditorTheme.cgFloat(list["baseIndentMultiplier"]) ?? 1
|
|
25
43
|
let listDepth = max(0, blockStack.filter { $0.listContext != nil }.count - 1)
|
|
26
|
-
|
|
44
|
+
// Preserve the legacy mixed-list baseline, then add each container's rule adjustment.
|
|
45
|
+
let listInset = listName == nil ? 0 : indent * (CGFloat(listDepth) + multiplier) + contextualIndent + listMarkerWidth(
|
|
46
|
+
for: context,
|
|
47
|
+
theme: theme,
|
|
48
|
+
baseFont: baseFont,
|
|
49
|
+
nestingDepth: listDepth,
|
|
50
|
+
ancestors: markerAncestors
|
|
51
|
+
)
|
|
27
52
|
style.headIndent = horizontal.left + listInset
|
|
28
53
|
style.firstLineHeadIndent = style.headIndent
|
|
29
54
|
style.tailIndent = -horizontal.right
|
|
@@ -180,35 +205,45 @@ extension RenderBridge {
|
|
|
180
205
|
mutableAttrs[RenderBridgeAttributes.listContext] = listContext
|
|
181
206
|
}
|
|
182
207
|
if let markerContext = currentBlock.listMarkerContext {
|
|
208
|
+
let ownerIndex = blockStack.lastIndex { $0.listContext != nil }
|
|
209
|
+
let markerAncestors = ownerIndex.map { blockStack.prefix($0 + 1).map(\.nodeType) } ?? []
|
|
210
|
+
let marker = theme?.styleSheet?.resolvedValues("listMarker", ancestors: markerAncestors)
|
|
211
|
+
let orderedTheme = (marker?["ordered"] as? [String: Any]).map(EditorOrderedListMarkerTheme.init(dictionary:)) ?? theme?.list?.orderedMarker
|
|
183
212
|
mutableAttrs[RenderBridgeAttributes.listMarkerContext] = markerContext
|
|
213
|
+
let ordered = (markerContext["ordered"] as? NSNumber)?.boolValue == true
|
|
184
214
|
let visualListDepth = max(0, blockStack.filter { $0.listContext != nil }.count - 1)
|
|
185
215
|
if (markerContext["kind"] as? String) != "task",
|
|
186
|
-
|
|
216
|
+
ordered,
|
|
187
217
|
let rawIndex = markerContext["index"] as? NSNumber,
|
|
188
218
|
let index = v2ExactUInt32(rawIndex) {
|
|
189
219
|
mutableAttrs[RenderBridgeAttributes.orderedListMarkerLabel] =
|
|
190
220
|
OrderedListMarkerFormatter.label(
|
|
191
221
|
index: index,
|
|
192
222
|
nestingDepth: visualListDepth,
|
|
193
|
-
theme:
|
|
223
|
+
theme: orderedTheme
|
|
194
224
|
)
|
|
195
225
|
}
|
|
196
|
-
mutableAttrs[RenderBridgeAttributes.listMarkerColor] = theme?.list?.markerColor
|
|
226
|
+
mutableAttrs[RenderBridgeAttributes.listMarkerColor] = EditorTheme.color(from: marker?["color"]) ?? theme?.list?.markerColor
|
|
197
227
|
mutableAttrs[RenderBridgeAttributes.listMarkerScale] = theme?.list?.markerScale
|
|
198
228
|
if let sheet = theme?.styleSheet {
|
|
199
|
-
mutableAttrs[RenderBridgeAttributes.listMarkerScale] =
|
|
229
|
+
mutableAttrs[RenderBridgeAttributes.listMarkerScale] = ordered
|
|
230
|
+
? 1
|
|
231
|
+
: EditorTheme.cgFloat(sheet.resolvedValues("listMarker", ancestors: markerAncestors)["scale"])
|
|
232
|
+
?? LayoutConstants.unorderedListMarkerFontScale
|
|
200
233
|
}
|
|
201
|
-
mutableAttrs[RenderBridgeAttributes.listMarkerGap] = theme?.list?.markerGap
|
|
234
|
+
mutableAttrs[RenderBridgeAttributes.listMarkerGap] = EditorTheme.cgFloat(marker?["gap"]) ?? theme?.list?.markerGap
|
|
202
235
|
mutableAttrs[RenderBridgeAttributes.listMarkerBaseFont] = paragraphBaseFont
|
|
203
236
|
if let sheet = theme?.styleSheet, (markerContext["kind"] as? String) == "task" {
|
|
204
|
-
let checkbox = sheet.checkbox(checked: markerContext["checked"] as? Bool == true)
|
|
237
|
+
let checkbox = sheet.checkbox(checked: markerContext["checked"] as? Bool == true, ancestors: markerAncestors)
|
|
205
238
|
mutableAttrs[editorTaskCheckboxAttribute] = EditorMentionRenderedBox(box: checkbox)
|
|
206
239
|
mutableAttrs[RenderBridgeAttributes.listMarkerGap] = checkbox.number("gap", fallback: 8)
|
|
207
240
|
}
|
|
208
241
|
mutableAttrs[RenderBridgeAttributes.listMarkerWidth] = listMarkerWidth(
|
|
209
242
|
for: currentBlock,
|
|
210
243
|
theme: theme,
|
|
211
|
-
baseFont: paragraphBaseFont
|
|
244
|
+
baseFont: paragraphBaseFont,
|
|
245
|
+
nestingDepth: visualListDepth,
|
|
246
|
+
ancestors: markerAncestors
|
|
212
247
|
)
|
|
213
248
|
}
|
|
214
249
|
if currentBlock.nodeType == "codeBlock", theme?.styleSheet == nil {
|
|
@@ -267,23 +302,30 @@ extension RenderBridge {
|
|
|
267
302
|
static func listMarkerWidth(
|
|
268
303
|
for context: BlockContext,
|
|
269
304
|
theme: EditorTheme?,
|
|
270
|
-
baseFont: UIFont
|
|
305
|
+
baseFont: UIFont,
|
|
306
|
+
nestingDepth: Int? = nil,
|
|
307
|
+
ancestors: [String] = []
|
|
271
308
|
) -> CGFloat {
|
|
272
309
|
guard let listContext = context.listContext else { return 0 }
|
|
273
310
|
if let sheet = theme?.styleSheet {
|
|
274
311
|
if (listContext["kind"] as? String) == "task" {
|
|
275
|
-
let box = sheet.checkbox(checked: listContext["checked"] as? Bool == true)
|
|
312
|
+
let box = sheet.checkbox(checked: listContext["checked"] as? Bool == true, ancestors: ancestors)
|
|
276
313
|
return box.number("size", fallback: 24) + box.number("gap", fallback: 8)
|
|
277
314
|
}
|
|
278
|
-
let ordered = (listContext["ordered"] as?
|
|
279
|
-
let scale =
|
|
280
|
-
|
|
315
|
+
let ordered = (listContext["ordered"] as? NSNumber)?.boolValue == true
|
|
316
|
+
let scale = ordered
|
|
317
|
+
? 1
|
|
318
|
+
: EditorTheme.cgFloat(sheet.resolvedValues("listMarker", ancestors: ancestors)["scale"])
|
|
319
|
+
?? LayoutConstants.unorderedListMarkerFontScale
|
|
320
|
+
let gap = EditorTheme.cgFloat(sheet.resolvedValues("listMarker", ancestors: ancestors)["gap"]) ?? 8
|
|
281
321
|
if !ordered {
|
|
282
322
|
return EditorLayoutManager.unorderedBulletDrawingRect(usedRect: .zero, lineFragmentRect: .zero, markerWidth: 0, baselineY: 0, baseFont: baseFont, markerScale: scale, origin: .zero).width + gap
|
|
283
323
|
}
|
|
284
|
-
let label =
|
|
285
|
-
|
|
286
|
-
:
|
|
324
|
+
let label = OrderedListMarkerFormatter.label(
|
|
325
|
+
index: jsonUInt32(listContext["index"]) ?? 1,
|
|
326
|
+
nestingDepth: nestingDepth ?? Int(context.depth),
|
|
327
|
+
theme: (sheet.resolvedValues("listMarker", ancestors: ancestors)["ordered"] as? [String: Any]).map(EditorOrderedListMarkerTheme.init(dictionary:)) ?? theme?.list?.orderedMarker
|
|
328
|
+
)
|
|
287
329
|
return ceil((label as NSString).size(withAttributes: [.font: baseFont.withSize(baseFont.pointSize * scale)]).width) + gap
|
|
288
330
|
}
|
|
289
331
|
return LayoutConstants.listMarkerWidth
|
package/ios/RenderBridge.swift
CHANGED
|
@@ -236,20 +236,18 @@ final class RenderBridge {
|
|
|
236
236
|
if let sheet = theme?.styleSheet {
|
|
237
237
|
var base = defaultAttributes(baseFont: blockFont, textColor: blockColor)
|
|
238
238
|
EditorStyleSheet.applyText(sheet.textValues(blockStack.last?.nodeType ?? "paragraph", ancestors: blockStack.dropLast().map(\.nodeType)), to: &base)
|
|
239
|
-
baseAttrs = sheet.inlineAttributes(marks, base: base)
|
|
239
|
+
baseAttrs = sheet.inlineAttributes(marks, base: base, ancestors: blockStack.map(\.nodeType))
|
|
240
240
|
}
|
|
241
241
|
if isCodeBlock {
|
|
242
|
-
//
|
|
243
|
-
// explicit code-block family for ordinary marked text;
|
|
244
|
-
// otherwise use the shared monospace resolver. In either
|
|
245
|
-
// case, the resolver accepts a face only if it satisfies
|
|
246
|
-
// the complete bold/italic request.
|
|
242
|
+
// Preserve explicit code families through the final monospace fallback.
|
|
247
243
|
let resolvedFont = baseAttrs[.font] as? UIFont ?? blockFont
|
|
248
244
|
let markTraits = resolvedFont.fontDescriptor.symbolicTraits
|
|
249
245
|
.intersection([.traitBold, .traitItalic])
|
|
250
|
-
let
|
|
246
|
+
let codeValues = theme?.styleSheet?.resolvedValues("codeBlock", ancestors: blockStack.dropLast().map(\.nodeType))
|
|
247
|
+
let themedFamily = (codeValues?["fontFamily"] as? String) ?? theme?.codeBlock?.text?.fontFamily
|
|
248
|
+
let hasInlineFamily = theme?.styleSheet?.hasInlineFontRule(marks, ancestors: blockStack.map(\.nodeType)) == true
|
|
251
249
|
baseAttrs[.font] = ViewerFontEnvironment.shared.resolveFont(
|
|
252
|
-
family: themedFamily ? nil : "monospace",
|
|
250
|
+
family: themedFamily != nil || hasInlineFamily ? nil : "monospace",
|
|
253
251
|
size: resolvedFont.pointSize,
|
|
254
252
|
fallback: resolvedFont,
|
|
255
253
|
additionalTraits: markTraits,
|
|
@@ -336,21 +334,23 @@ final class RenderBridge {
|
|
|
336
334
|
topLevelChildIndex: topLevelChildIndex,
|
|
337
335
|
theme: theme,
|
|
338
336
|
atomKey: atomKey,
|
|
339
|
-
atomConfiguration: atomConfiguration
|
|
337
|
+
atomConfiguration: atomConfiguration,
|
|
338
|
+
ancestors: blockStack.map(\.nodeType)
|
|
340
339
|
)
|
|
341
340
|
if let sheet = theme?.styleSheet {
|
|
342
341
|
let styled = NSMutableAttributedString(attributedString: attrStr)
|
|
343
342
|
styled.addAttribute(editorStyledContentAttribute, value: true, range: NSRange(location: 0, length: styled.length))
|
|
344
343
|
let context = BlockContext(nodeType: nodeType, depth: blockStack.last?.depth ?? 0, listContext: nil)
|
|
345
344
|
let style = paragraphStyleForBlock(context, blockStack: blockStack + [context], theme: theme, baseFont: baseFont)
|
|
346
|
-
let
|
|
345
|
+
let box = sheet.box(nodeType, ancestors: blockStack.map(\.nodeType))
|
|
346
|
+
let inset = box.inset
|
|
347
347
|
style.headIndent -= inset.left
|
|
348
348
|
style.firstLineHeadIndent -= inset.left
|
|
349
349
|
style.tailIndent += inset.right
|
|
350
|
-
style.paragraphSpacingBefore =
|
|
351
|
-
style.paragraphSpacing =
|
|
350
|
+
style.paragraphSpacingBefore = box.margin.top
|
|
351
|
+
style.paragraphSpacing = box.margin.bottom
|
|
352
352
|
styled.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: styled.length))
|
|
353
|
-
styled.addAttribute(editorBlockSpacingBoxAttribute, value: EditorRenderedBox(box:
|
|
353
|
+
styled.addAttribute(editorBlockSpacingBoxAttribute, value: EditorRenderedBox(box: box, depth: blockStack.count, leading: 0, trailing: 0), range: NSRange(location: 0, length: styled.length))
|
|
354
354
|
result.append(styled)
|
|
355
355
|
} else { result.append(attrStr) }
|
|
356
356
|
pendingTrailingParagraphSpacing = theme?.effectiveTextStyle(
|
|
@@ -512,7 +512,9 @@ final class RenderBridge {
|
|
|
512
512
|
}
|
|
513
513
|
|
|
514
514
|
case "blockEnd":
|
|
515
|
-
if let endedBlock = blockStack.
|
|
515
|
+
if let endedBlock = blockStack.last {
|
|
516
|
+
let endedAncestors = Array(blockStack.dropLast())
|
|
517
|
+
blockStack.removeLast()
|
|
516
518
|
appendTrailingLineBreakPlaceholderIfNeeded(
|
|
517
519
|
in: result,
|
|
518
520
|
endedBlock: endedBlock,
|
|
@@ -525,15 +527,16 @@ final class RenderBridge {
|
|
|
525
527
|
&& blockStack.last?.nodeType == "blockquote"
|
|
526
528
|
&& elements.indices.contains(elementIndex + 1)
|
|
527
529
|
&& elements[elementIndex + 1]["type"] as? String == "blockEnd"
|
|
528
|
-
closeStyledBlock(endedBlock, ancestors:
|
|
530
|
+
closeStyledBlock(endedBlock, ancestors: endedAncestors, in: result, theme: theme, baseFont: baseFont, textColor: textColor, omitBottomMargin: omitBottomMargin)
|
|
529
531
|
if EditorStyleSheet.element(endedBlock.nodeType) == "codeBlock", endedBlock.styleStart < result.length {
|
|
530
532
|
result.addAttribute(editorCodeBlockAttribute, value: EditorCodeBlockPresentation(language: endedBlock.language), range: NSRange(location: endedBlock.styleStart, length: result.length - endedBlock.styleStart))
|
|
531
533
|
}
|
|
532
534
|
if theme?.styleSheet != nil, endedBlock.listContext?["isLast"] as? Bool == true,
|
|
533
535
|
let container = blockStack.last,
|
|
534
536
|
["bulletList", "orderedList", "taskList"].contains(container.nodeType) {
|
|
537
|
+
let containerAncestors = Array(blockStack.dropLast())
|
|
535
538
|
blockStack.removeLast()
|
|
536
|
-
closeStyledBlock(container, ancestors:
|
|
539
|
+
closeStyledBlock(container, ancestors: containerAncestors, in: result, theme: theme, baseFont: baseFont, textColor: textColor)
|
|
537
540
|
}
|
|
538
541
|
if endedBlock.listContext != nil, theme?.styleSheet == nil {
|
|
539
542
|
let spacing = (endedBlock.listContext?["isLast"] as? Bool) == true
|