@apollohg/react-native-rich-text-editor 2.0.4 → 2.0.6
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/README.md +11 -0
- 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 +2 -2
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +2 -2
- package/ios/EditorLayoutManager.swift +2 -4
- package/ios/EditorStyleSheet.swift +77 -11
- package/ios/EditorTextView+Commands.swift +1 -1
- 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/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.mm +1 -0
- 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,
|
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
|
|
@@ -13,7 +13,8 @@ extension CoreTextProseLayoutEngine {
|
|
|
13
13
|
_ inlines: [ViewerInline],
|
|
14
14
|
paint: PreparedTextPaint,
|
|
15
15
|
theme: PreparedProseTheme,
|
|
16
|
-
warningSemanticGeneration: String
|
|
16
|
+
warningSemanticGeneration: String,
|
|
17
|
+
ancestors: [String] = []
|
|
17
18
|
) -> PreparedAttributedBlock {
|
|
18
19
|
let result = NSMutableAttributedString()
|
|
19
20
|
var atoms: [PreparedAtomSpec] = []
|
|
@@ -39,7 +40,7 @@ extension CoreTextProseLayoutEngine {
|
|
|
39
40
|
switch inline {
|
|
40
41
|
case let .text(text: text, marks: marks):
|
|
41
42
|
let start = result.length
|
|
42
|
-
result.append(NSAttributedString(string: text, attributes: attributes(for: marks, paint: paint, theme: theme, warningSemanticGeneration: warningSemanticGeneration)))
|
|
43
|
+
result.append(NSAttributedString(string: text, attributes: attributes(for: marks, paint: paint, theme: theme, warningSemanticGeneration: warningSemanticGeneration, ancestors: ancestors)))
|
|
43
44
|
let range = NSRange(location: start, length: (text as NSString).length)
|
|
44
45
|
if let href = href(in: marks), !text.isEmpty {
|
|
45
46
|
let semanticIndex: Int
|
|
@@ -67,7 +68,8 @@ extension CoreTextProseLayoutEngine {
|
|
|
67
68
|
attrsJSON: attrsJSON,
|
|
68
69
|
paint: paint,
|
|
69
70
|
theme: theme,
|
|
70
|
-
warningSemanticGeneration: warningSemanticGeneration
|
|
71
|
+
warningSemanticGeneration: warningSemanticGeneration,
|
|
72
|
+
ancestors: ancestors
|
|
71
73
|
)
|
|
72
74
|
let displayLabel = label.isEmpty ? " " : label
|
|
73
75
|
let labelLine = CTLineCreateWithAttributedString(
|
|
@@ -137,7 +139,7 @@ extension CoreTextProseLayoutEngine {
|
|
|
137
139
|
return nil
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
func attributes(for marks: [FfiViewerMark], paint: PreparedTextPaint, theme: PreparedProseTheme, warningSemanticGeneration: String) -> [NSAttributedString.Key: Any] {
|
|
142
|
+
func attributes(for marks: [FfiViewerMark], paint: PreparedTextPaint, theme: PreparedProseTheme, warningSemanticGeneration: String, ancestors: [String] = []) -> [NSAttributedString.Key: Any] {
|
|
141
143
|
if let sheet = theme.styleSheet {
|
|
142
144
|
var base: [NSAttributedString.Key: Any] = [.font: paint.font, .foregroundColor: paint.color]
|
|
143
145
|
EditorStyleSheet.applyText(paint.textValues, to: &base, scale: theme.fontScale)
|
|
@@ -146,7 +148,7 @@ extension CoreTextProseLayoutEngine {
|
|
|
146
148
|
values["type"] = mark.markType
|
|
147
149
|
return values
|
|
148
150
|
}
|
|
149
|
-
var resolved = sheet.inlineAttributes(markValues, base: base, scale: theme.fontScale)
|
|
151
|
+
var resolved = sheet.inlineAttributes(markValues, base: base, scale: theme.fontScale, ancestors: ancestors)
|
|
150
152
|
for mark in marks {
|
|
151
153
|
let values = jsonDictionary(mark.attrsJson)
|
|
152
154
|
switch mark.markType {
|
|
@@ -313,11 +315,18 @@ extension CoreTextProseLayoutEngine {
|
|
|
313
315
|
_ context: ViewerListContext,
|
|
314
316
|
nestingDepth: Int,
|
|
315
317
|
paint: PreparedTextPaint,
|
|
316
|
-
theme: PreparedProseTheme
|
|
318
|
+
theme: PreparedProseTheme,
|
|
319
|
+
ancestors: [String] = []
|
|
317
320
|
) -> PreparedListMarker {
|
|
321
|
+
let marker = theme.styleSheet?.resolvedValues("listMarker", ancestors: ancestors) ?? [:]
|
|
322
|
+
let ordered = (marker["ordered"] as? [String: Any]).map(EditorOrderedListMarkerTheme.init(dictionary:)) ?? theme.orderedListMarker
|
|
323
|
+
let color = EditorTheme.color(from: marker["color"]) ?? theme.listMarkerColor
|
|
318
324
|
let scale: CGFloat
|
|
319
|
-
if
|
|
320
|
-
scale =
|
|
325
|
+
if theme.styleSheet != nil {
|
|
326
|
+
scale = !context.ordered && context.kind != "task"
|
|
327
|
+
? EditorTheme.cgFloat(marker["scale"])
|
|
328
|
+
?? LayoutConstants.unorderedListMarkerFontScale
|
|
329
|
+
: 1
|
|
321
330
|
if !context.ordered, context.kind != "task" {
|
|
322
331
|
let diameter = EditorLayoutManager.unorderedBulletDrawingRect(usedRect: .zero, lineFragmentRect: .zero, markerWidth: 0, baselineY: 0, baseFont: paint.font, markerScale: scale, origin: .zero).width
|
|
323
332
|
return PreparedListMarker(line: nil, label: "•", width: diameter, ascent: diameter / 2, descent: diameter / 2, checked: false)
|
|
@@ -333,13 +342,13 @@ extension CoreTextProseLayoutEngine {
|
|
|
333
342
|
label = OrderedListMarkerFormatter.label(
|
|
334
343
|
index: UInt32(exactly: context.index) ?? 0,
|
|
335
344
|
nestingDepth: nestingDepth,
|
|
336
|
-
theme:
|
|
345
|
+
theme: ordered
|
|
337
346
|
)
|
|
338
347
|
} else {
|
|
339
348
|
label = "•"
|
|
340
349
|
}
|
|
341
350
|
guard !label.isEmpty else {
|
|
342
|
-
let side = theme.styleSheet?.checkbox(checked: context.checked).number("size", fallback: 24) ?? max(font.lineHeight, font.pointSize)
|
|
351
|
+
let side = theme.styleSheet?.checkbox(checked: context.checked, ancestors: ancestors).number("size", fallback: 24) ?? max(font.lineHeight, font.pointSize)
|
|
343
352
|
return PreparedListMarker(line: nil, label: label, width: side, ascent: side * 0.75, descent: side * 0.25, checked: context.checked)
|
|
344
353
|
}
|
|
345
354
|
let line = CTLineCreateWithAttributedString(
|
|
@@ -347,7 +356,7 @@ extension CoreTextProseLayoutEngine {
|
|
|
347
356
|
string: label,
|
|
348
357
|
attributes: [
|
|
349
358
|
kCTFontAttributeName as NSAttributedString.Key: Self.coreTextFont(from: font),
|
|
350
|
-
kCTForegroundColorAttributeName as NSAttributedString.Key:
|
|
359
|
+
kCTForegroundColorAttributeName as NSAttributedString.Key: color.cgColor
|
|
351
360
|
]
|
|
352
361
|
)
|
|
353
362
|
)
|
|
@@ -366,12 +375,21 @@ extension CoreTextProseLayoutEngine {
|
|
|
366
375
|
attrsJSON: String,
|
|
367
376
|
paint: PreparedTextPaint,
|
|
368
377
|
theme: PreparedProseTheme,
|
|
369
|
-
warningSemanticGeneration: String
|
|
378
|
+
warningSemanticGeneration: String,
|
|
379
|
+
ancestors: [String]
|
|
370
380
|
) -> PreparedAtomAppearance {
|
|
371
381
|
if nodeType == "mention" {
|
|
372
382
|
let values = jsonDictionary(attrsJSON)
|
|
373
383
|
let localMention = (values["mentionTheme"] as? [String: Any]).map(EditorMentionTheme.init(dictionary:))
|
|
374
|
-
|
|
384
|
+
var globalMention = theme.mention
|
|
385
|
+
if let sheet = theme.styleSheet, !sheet.rules.isEmpty {
|
|
386
|
+
let projection = EditorTheme.legacyProjection(
|
|
387
|
+
styles: ["mention": sheet.resolvedValues("mention", ancestors: ancestors)],
|
|
388
|
+
root: theme.mentionOverrides.map { ["mentions": $0] } ?? [:]
|
|
389
|
+
)
|
|
390
|
+
globalMention = (projection["mentions"] as? [String: Any]).map(EditorMentionTheme.init(dictionary:))
|
|
391
|
+
}
|
|
392
|
+
let mention = (globalMention?.merged(with: localMention) ?? localMention)?.node
|
|
375
393
|
var attributes = baseAttributes(paint)
|
|
376
394
|
if let weight = mention?.fontWeight {
|
|
377
395
|
let font = ViewerFontEnvironment.shared.resolveFont(
|