@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
|
@@ -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(
|
|
@@ -138,6 +138,7 @@ struct PreparedProseTheme {
|
|
|
138
138
|
let ruleMargin: CGFloat
|
|
139
139
|
let link: EditorLinkTheme?
|
|
140
140
|
let mention: EditorMentionTheme?
|
|
141
|
+
let mentionOverrides: [String: Any]?
|
|
141
142
|
|
|
142
143
|
static func resolve(
|
|
143
144
|
themeJSON: String?,
|
|
@@ -205,7 +206,7 @@ struct PreparedProseTheme {
|
|
|
205
206
|
headings: headings,
|
|
206
207
|
blockquote: quote,
|
|
207
208
|
code: paint(codeStyle, fallback: codeFallback),
|
|
208
|
-
contentInsets: UIEdgeInsets(
|
|
209
|
+
contentInsets: theme.styleSheet?.box("content").inset ?? UIEdgeInsets(
|
|
209
210
|
top: theme.contentInsets?.top ?? 0,
|
|
210
211
|
left: theme.contentInsets?.left ?? 0,
|
|
211
212
|
bottom: theme.contentInsets?.bottom ?? 0,
|
|
@@ -231,7 +232,8 @@ struct PreparedProseTheme {
|
|
|
231
232
|
ruleThickness: theme.horizontalRule?.thickness ?? 1,
|
|
232
233
|
ruleMargin: theme.horizontalRule?.verticalMargin ?? 12,
|
|
233
234
|
link: theme.links,
|
|
234
|
-
mention: theme.mentions
|
|
235
|
+
mention: theme.mentions,
|
|
236
|
+
mentionOverrides: theme.styleSheetMentionOverrides
|
|
235
237
|
)
|
|
236
238
|
}
|
|
237
239
|
|
|
@@ -319,4 +321,17 @@ struct PreparedListMarker {
|
|
|
319
321
|
let checked: Bool
|
|
320
322
|
}
|
|
321
323
|
|
|
324
|
+
extension ViewerBlock {
|
|
325
|
+
func ancestors(before ancestor: ViewerStyleAncestor) -> [String] {
|
|
326
|
+
styleAncestors.prefix { $0.identity != ancestor.identity }.map(\.nodeType)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
var markerStyleAncestors: [String] {
|
|
330
|
+
guard let item = styleAncestors.lastIndex(where: { ["listItem", "taskItem"].contains(EditorStyleSheet.element($0.nodeType)) }) else {
|
|
331
|
+
return styleAncestors.map(\.nodeType)
|
|
332
|
+
}
|
|
333
|
+
return styleAncestors.prefix(item + 1).map(\.nodeType)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
322
337
|
/// Performs the width-dependent, immutable Core Text preparation step.
|
|
@@ -66,7 +66,8 @@ final class CoreTextProseLayoutEngine {
|
|
|
66
66
|
context,
|
|
67
67
|
nestingDepth: markerNestingDepth,
|
|
68
68
|
paint: theme.paint(for: block),
|
|
69
|
-
theme: theme
|
|
69
|
+
theme: theme,
|
|
70
|
+
ancestors: block.markerStyleAncestors
|
|
70
71
|
)
|
|
71
72
|
}
|
|
72
73
|
for (index, block) in document.blocks.enumerated() {
|
|
@@ -90,14 +91,14 @@ final class CoreTextProseLayoutEngine {
|
|
|
90
91
|
let shared = zip(previous.styleAncestors, block.styleAncestors).prefix { $0 == $1 }.count
|
|
91
92
|
let previousSibling = previous.styleAncestors.dropFirst(shared).first?.nodeType ?? previous.nodeType
|
|
92
93
|
let nextSibling = block.styleAncestors.dropFirst(shared).first?.nodeType ?? block.nodeType
|
|
93
|
-
let previousMargin = sheet.box(previousSibling).margin.bottom
|
|
94
|
-
let nextMargin = sheet.box(nextSibling).margin.top
|
|
94
|
+
let previousMargin = sheet.box(previousSibling, ancestors: previous.styleAncestors.prefix(shared).map(\.nodeType)).margin.bottom
|
|
95
|
+
let nextMargin = sheet.box(nextSibling, ancestors: block.styleAncestors.prefix(shared).map(\.nodeType)).margin.top
|
|
95
96
|
cursorY -= previousMargin + nextMargin - EditorStyleSheet.collapsedMargin(previousMargin, nextMargin)
|
|
96
97
|
}
|
|
97
98
|
let omitBottomMargin = block.nodeType == "paragraph"
|
|
98
99
|
&& block.styleAncestors.last.map { $0.nodeType == "blockquote" && closing.contains($0) } == true
|
|
99
|
-
let top = opening.reduce(CGFloat.zero) { $0 + (theme.styleSheet?.box($1.nodeType).outerInsets.top ?? 0) }
|
|
100
|
-
let bottom = closing.reduce(CGFloat.zero) { $0 + (theme.styleSheet?.box($1.nodeType).outerInsets.bottom ?? 0) }
|
|
100
|
+
let top = opening.reduce(CGFloat.zero) { $0 + (theme.styleSheet?.box($1.nodeType, ancestors: block.ancestors(before: $1)).outerInsets.top ?? 0) }
|
|
101
|
+
let bottom = closing.reduce(CGFloat.zero) { $0 + (theme.styleSheet?.box($1.nodeType, ancestors: block.ancestors(before: $1)).outerInsets.bottom ?? 0) }
|
|
101
102
|
cursorY += top
|
|
102
103
|
let prepared = prepareBlock(
|
|
103
104
|
block,
|
|
@@ -128,10 +129,10 @@ final class CoreTextProseLayoutEngine {
|
|
|
128
129
|
var outerLeft: CGFloat = 0
|
|
129
130
|
var outerRight: CGFloat = 0
|
|
130
131
|
for (depth, ancestor) in block.styleAncestors.enumerated() {
|
|
131
|
-
let box = sheet.box(ancestor.nodeType)
|
|
132
|
+
let box = sheet.box(ancestor.nodeType, ancestors: block.ancestors(before: ancestor))
|
|
132
133
|
let remaining = block.styleAncestors.dropFirst(depth + 1)
|
|
133
|
-
let innerTop = remaining.reduce(CGFloat.zero) { $0 + (opening.contains($1) ? sheet.box($1.nodeType).outerInsets.top : 0) }
|
|
134
|
-
let innerBottom = remaining.reduce(CGFloat.zero) { $0 + (closing.contains($1) ? sheet.box($1.nodeType).outerInsets.bottom : 0) }
|
|
134
|
+
let innerTop = remaining.reduce(CGFloat.zero) { $0 + (opening.contains($1) ? sheet.box($1.nodeType, ancestors: block.ancestors(before: $1)).outerInsets.top : 0) }
|
|
135
|
+
let innerBottom = remaining.reduce(CGFloat.zero) { $0 + (closing.contains($1) ? sheet.box($1.nodeType, ancestors: block.ancestors(before: $1)).outerInsets.bottom : 0) }
|
|
135
136
|
let y = cursorY - innerTop - (opening.contains(ancestor) ? box.inset.top : 0)
|
|
136
137
|
let end = prepared.nextY + innerBottom + (closing.contains(ancestor) ? box.inset.bottom : 0)
|
|
137
138
|
let rect = CGRect(
|
|
@@ -211,8 +212,8 @@ final class CoreTextProseLayoutEngine {
|
|
|
211
212
|
warningSemanticGeneration: String
|
|
212
213
|
) -> BlockPreparation {
|
|
213
214
|
let sheet = theme.styleSheet
|
|
214
|
-
let box = sheet?.box(block.nodeType) ?? EditorStyleBox()
|
|
215
|
-
let ancestors = block.styleAncestors.reduce(UIEdgeInsets.zero) { $0.adding(sheet?.box($1.nodeType).outerInsets ?? .zero) }
|
|
215
|
+
let box = sheet?.box(block.nodeType, ancestors: block.styleAncestors.map(\.nodeType)) ?? EditorStyleBox()
|
|
216
|
+
let ancestors = block.styleAncestors.reduce(UIEdgeInsets.zero) { $0.adding(sheet?.box($1.nodeType, ancestors: block.ancestors(before: $1)).outerInsets ?? .zero) }
|
|
216
217
|
let cursorY = cursorY + box.margin.top
|
|
217
218
|
let contentX = theme.contentInsets.left + ancestors.left + box.margin.left
|
|
218
219
|
let contentWidth = max(1, width - theme.contentInsets.left - theme.contentInsets.right - ancestors.left - ancestors.right - box.margin.left - box.margin.right)
|
|
@@ -222,7 +223,7 @@ final class CoreTextProseLayoutEngine {
|
|
|
222
223
|
: (block.listItemBoundary.map { Int($0.nestingDepth) } ?? max(0, Int(block.depth) - 1))
|
|
223
224
|
let fallbackMarkerNestingDepth = max(0, block.listItemAncestors.count - 1)
|
|
224
225
|
let measuredListMarker = listMarker ?? block.listContext.map {
|
|
225
|
-
makeListMarker($0, nestingDepth: fallbackMarkerNestingDepth, paint: paint, theme: theme)
|
|
226
|
+
makeListMarker($0, nestingDepth: fallbackMarkerNestingDepth, paint: paint, theme: theme, ancestors: block.markerStyleAncestors)
|
|
226
227
|
}
|
|
227
228
|
let marker = block.listItemBoundary.map { $0.isFirstRenderableLeaf ? measuredListMarker : nil } ?? measuredListMarker
|
|
228
229
|
// The marker gutter is an independently measured column. In particular,
|
|
@@ -235,10 +236,26 @@ final class CoreTextProseLayoutEngine {
|
|
|
235
236
|
let baseMultiplier = EditorTheme.cgFloat(listValues["baseIndentMultiplier"]) ?? theme.listBaseIndentMultiplier
|
|
236
237
|
let listBaseIndent = block.listContext == nil ? 0 : max(0, listIndent * baseMultiplier)
|
|
237
238
|
let nestedListIndent = block.listContext == nil ? 0 : max(0, listIndent * CGFloat(listDepth))
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
// Keep the mixed-list baseline and add each container's contextual change.
|
|
240
|
+
var contextualListIndent: CGFloat = 0
|
|
241
|
+
if let sheet, block.listContext != nil {
|
|
242
|
+
let lists = block.styleAncestors.filter { ["bulletList", "orderedList", "taskList"].contains(EditorStyleSheet.element($0.nodeType)) }
|
|
243
|
+
for (depth, ancestor) in lists.enumerated() {
|
|
244
|
+
let base = sheet[ancestor.nodeType]
|
|
245
|
+
let resolved = sheet.resolvedValues(ancestor.nodeType, ancestors: block.ancestors(before: ancestor))
|
|
246
|
+
let baseIndent = EditorTheme.cgFloat(base["indent"]) ?? theme.listIndent
|
|
247
|
+
let resolvedIndent = EditorTheme.cgFloat(resolved["indent"]) ?? theme.listIndent
|
|
248
|
+
let baseMultiplier = depth == 0 ? EditorTheme.cgFloat(base["baseIndentMultiplier"]) ?? theme.listBaseIndentMultiplier : 1
|
|
249
|
+
let resolvedMultiplier = depth == 0 ? EditorTheme.cgFloat(resolved["baseIndentMultiplier"]) ?? theme.listBaseIndentMultiplier : 1
|
|
250
|
+
contextualListIndent += max(0, resolvedIndent * resolvedMultiplier) - max(0, baseIndent * baseMultiplier)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let markerValues = sheet?.resolvedValues("listMarker", ancestors: block.markerStyleAncestors) ?? [:]
|
|
254
|
+
let markerColor = EditorTheme.color(from: markerValues["color"]) ?? theme.listMarkerColor
|
|
255
|
+
let checkbox = block.listContext.flatMap { $0.kind == "task" ? sheet?.checkbox(checked: $0.checked, ancestors: block.markerStyleAncestors) : nil }
|
|
256
|
+
let markerGap = checkbox?.number("gap", fallback: 8) ?? EditorTheme.cgFloat(markerValues["gap"]) ?? theme.listMarkerGap
|
|
240
257
|
let markerGutter = measuredListMarker.map { max(markerGap, $0.width + markerGap) } ?? 0
|
|
241
|
-
let listInset = listBaseIndent + nestedListIndent + markerGutter
|
|
258
|
+
let listInset = listBaseIndent + nestedListIndent + contextualListIndent + markerGutter
|
|
242
259
|
let quoteInset = block.inBlockquote ? theme.quoteBorderWidth + theme.quoteMarkerGap + theme.quoteIndent : 0
|
|
243
260
|
let codeInset = block.nodeType == "codeBlock" ? theme.codePaddingHorizontal : 0
|
|
244
261
|
let textX = contentX + listInset + quoteInset + codeInset + box.inset.left
|
|
@@ -294,7 +311,7 @@ final class CoreTextProseLayoutEngine {
|
|
|
294
311
|
line: marker.line,
|
|
295
312
|
origin: CGPoint(x: markerX, y: markerTop + marker.ascent),
|
|
296
313
|
bounds: markerBounds,
|
|
297
|
-
color:
|
|
314
|
+
color: markerColor.cgColor,
|
|
298
315
|
label: marker.label,
|
|
299
316
|
checked: marker.checked,
|
|
300
317
|
styleBox: checkbox
|
|
@@ -334,12 +351,13 @@ final class CoreTextProseLayoutEngine {
|
|
|
334
351
|
return BlockPreparation(block: prepared, interactions: [], accessibilityNodes: [node], attachment: attachment, nextY: bounds.maxY + itemSpacing, retainedBytes: prepared.estimatedRetainedBytes + 192)
|
|
335
352
|
}
|
|
336
353
|
if block.nodeType == "horizontalRule" || block.nodeType == "horizontal_rule" {
|
|
354
|
+
let thickness = sheet == nil ? theme.ruleThickness : box.number("height", fallback: theme.ruleThickness)
|
|
337
355
|
let ruleX = contentX + listInset + quoteInset + box.inset.left
|
|
338
356
|
let ruleWidth = max(1, contentWidth - listInset - quoteInset - box.inset.left - box.inset.right)
|
|
339
357
|
let y = cursorY + theme.ruleMargin + box.inset.top
|
|
340
|
-
let rule = CGRect(x: ruleX, y: y, width: ruleWidth, height:
|
|
341
|
-
var fragments: [PreparedProseFragment] = [.init(kind: .rule, bounds: rule, color: theme.ruleColor.cgColor, strokeWidth:
|
|
342
|
-
let totalEnd = y +
|
|
358
|
+
let rule = CGRect(x: ruleX, y: y, width: ruleWidth, height: thickness)
|
|
359
|
+
var fragments: [PreparedProseFragment] = [.init(kind: .rule, bounds: rule, color: theme.ruleColor.cgColor, strokeWidth: thickness)]
|
|
360
|
+
let totalEnd = y + thickness + theme.ruleMargin + box.inset.bottom
|
|
343
361
|
if sheet != nil {
|
|
344
362
|
fragments = [.init(kind: .background, bounds: CGRect(x: contentX, y: cursorY, width: contentWidth, height: totalEnd - cursorY), styleBox: box)]
|
|
345
363
|
}
|
|
@@ -352,7 +370,7 @@ final class CoreTextProseLayoutEngine {
|
|
|
352
370
|
let markerTop = cursorY + (totalEnd - cursorY - markerHeight) / 2
|
|
353
371
|
let markerBaseline = markerTop + marker.ascent
|
|
354
372
|
let markerBounds = CGRect(x: markerX, y: markerTop, width: marker.width, height: markerHeight)
|
|
355
|
-
fragments.append(.init(kind: .marker, line: marker.line, origin: CGPoint(x: markerX, y: markerBaseline), bounds: markerBounds, color:
|
|
373
|
+
fragments.append(.init(kind: .marker, line: marker.line, origin: CGPoint(x: markerX, y: markerBaseline), bounds: markerBounds, color: markerColor.cgColor, label: marker.label, checked: marker.checked, styleBox: checkbox))
|
|
356
374
|
}
|
|
357
375
|
let seedBounds = CGRect(x: contentX, y: cursorY, width: contentWidth, height: totalEnd - cursorY)
|
|
358
376
|
let bounds = fragments.reduce(seedBounds) { $0.union($1.bounds) }
|
|
@@ -376,7 +394,7 @@ final class CoreTextProseLayoutEngine {
|
|
|
376
394
|
}
|
|
377
395
|
|
|
378
396
|
let availableWidth = max(1, contentWidth - listInset - quoteInset - codeInset * 2 - box.inset.left - box.inset.right)
|
|
379
|
-
let attributed = makeAttributedString(block.inlines, paint: paint, theme: theme, warningSemanticGeneration: warningSemanticGeneration)
|
|
397
|
+
let attributed = makeAttributedString(block.inlines, paint: paint, theme: theme, warningSemanticGeneration: warningSemanticGeneration, ancestors: block.styleAncestors.map(\.nodeType) + [block.nodeType])
|
|
380
398
|
let highlighted = NSMutableAttributedString(attributedString: attributed.string)
|
|
381
399
|
NativeCodeHighlightPresentation.apply(highlighting, to: highlighted)
|
|
382
400
|
let typesetter = CTTypesetterCreateWithAttributedString(highlighted)
|
|
@@ -510,7 +528,7 @@ final class CoreTextProseLayoutEngine {
|
|
|
510
528
|
width: marker.width,
|
|
511
529
|
height: markerHeight
|
|
512
530
|
)
|
|
513
|
-
fragments.append(.init(kind: .marker, line: marker.line, origin: CGPoint(x: markerX, y: markerBaseline), bounds: markerBounds, color:
|
|
531
|
+
fragments.append(.init(kind: .marker, line: marker.line, origin: CGPoint(x: markerX, y: markerBaseline), bounds: markerBounds, color: markerColor.cgColor, label: marker.label, checked: marker.checked, styleBox: checkbox))
|
|
514
532
|
}
|
|
515
533
|
let seedBounds = CGRect(x: contentX, y: cursorY, width: contentWidth, height: max(0, totalEnd - cursorY))
|
|
516
534
|
let bounds = fragments.reduce(seedBounds) { $0.union($1.bounds) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-rich-text-editor",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Native rich text editor with Rust core for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/apollohg/react-native-rich-text-editor",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"install:example:pods": "cd example/ios && pod install --no-repo-update",
|
|
48
48
|
"install:ios-test-pods": "cd ios-tests && pod install --no-repo-update",
|
|
49
49
|
"lint:android": "npm run prebuild:example:android && cd example/android && ./gradlew :apollohg_react-native-rich-text-editor:lintDebug && node ../../scripts/assert-android-lint-clean.mjs ../../android/build/reports/lint-results-debug.xml",
|
|
50
|
-
"lint:ios": "swiftlint lint --strict",
|
|
50
|
+
"lint:ios": "node scripts/swiftlint.mjs lint --strict",
|
|
51
51
|
"lint:kotlin": "node scripts/ktlint.mjs",
|
|
52
52
|
"prebuild:example": "npm --prefix example run prebuild",
|
|
53
53
|
"prebuild:example:android": "npm --prefix example run prebuild:android",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"test:rust": "cargo test --manifest-path rust/editor-core/Cargo.toml",
|
|
76
76
|
"typecheck": "tsc --noEmit",
|
|
77
77
|
"typecheck:example": "npm --prefix example run typecheck",
|
|
78
|
-
"validate:package": "npm run prepare:example:native && npm run install:ios-test-pods && node scripts/tests/validate-prepared-prose-viewer-cutover.mjs && npm run validate:security -- --release --behavior && node scripts/tests/validate-security-contracts-negative.mjs && node scripts/tests/validate-publish-lifecycle.mjs && bash ./scripts/tests/validate-generated-bindings.sh && bash ./scripts/tests/validate-packed-xcframework.sh && node scripts/tests/validate-packed-package.test.mjs && bash ./scripts/validate-packed-package.sh",
|
|
78
|
+
"validate:package": "npm run prepare:example:native && npm run install:ios-test-pods && npm run lint:ios && npm run lint:kotlin && node scripts/tests/validate-prepared-prose-viewer-cutover.mjs && npm run validate:security -- --release --behavior && node scripts/tests/validate-security-contracts-negative.mjs && node scripts/tests/validate-publish-lifecycle.mjs && bash ./scripts/tests/validate-generated-bindings.sh && bash ./scripts/tests/validate-packed-xcframework.sh && node scripts/tests/validate-packed-package.test.mjs && bash ./scripts/validate-packed-package.sh",
|
|
79
79
|
"validate:package:contracts": "bash ./scripts/validate-packed-package.sh --validate-packed-tarball \"$RELEASE_TARBALL\"",
|
|
80
80
|
"validate:package:ios:positive": "bash ./scripts/validate-packed-package.sh --validate-ios-consumer . \"$RELEASE_TARBALL\"",
|
|
81
81
|
"validate:package:ios:negative": "VALIDATE_PACKED_PACKAGE_GROUP=ios-consumer node scripts/tests/validate-packed-package.test.mjs",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -319,6 +319,56 @@ export function normalizeEditorTheme(theme: unknown): NormalizedEditorTheme {
|
|
|
319
319
|
const result: NormalizedEditorTheme = { version: 1 };
|
|
320
320
|
|
|
321
321
|
for (const [ key, value ] of Object.entries(theme)) {
|
|
322
|
+
if (key === 'rules') {
|
|
323
|
+
if (value === undefined) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!Array.isArray(value)) {
|
|
328
|
+
invalid('rules', 'expected an array');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
result.rules = value.map((entry, index) => {
|
|
332
|
+
const indexedPath = `rules[${index}]`;
|
|
333
|
+
|
|
334
|
+
if (!record(entry)) {
|
|
335
|
+
invalid(indexedPath, 'expected a rule object');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const propertyName of Object.keys(entry)) {
|
|
339
|
+
if (propertyName !== 'path' && propertyName !== 'style') {
|
|
340
|
+
invalid(`${indexedPath}.${propertyName}`, 'unsupported property');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!Array.isArray(entry.path) || entry.path.length === 0) {
|
|
345
|
+
invalid(`${indexedPath}.path`, 'expected a nonempty array');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
entry.path.forEach((element, pathIndex) => {
|
|
349
|
+
if (
|
|
350
|
+
typeof element !== 'string' ||
|
|
351
|
+
!Object.prototype.hasOwnProperty.call(fields, element)
|
|
352
|
+
) {
|
|
353
|
+
invalid(`${indexedPath}.path[${pathIndex}]`, 'unsupported element');
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'style')) {
|
|
358
|
+
invalid(`${indexedPath}.style`, 'expected an own style property');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const target = entry.path[entry.path.length - 1] as keyof EditorStyleMap;
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
path: [ ...entry.path ],
|
|
365
|
+
style: normalizeStyle(entry.style, fields[target], `${indexedPath}.style`),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
322
372
|
if (key === 'toolbar') {
|
|
323
373
|
if (value !== undefined) {
|
|
324
374
|
if (!record(value)) {
|
|
@@ -146,9 +146,19 @@ export interface EditorStyleMap {
|
|
|
146
146
|
placeholder: EditorTypographyStyle;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export type EditorStyleRule = {
|
|
150
|
+
[K in keyof EditorStyleMap]: {
|
|
151
|
+
path: readonly [ ...(keyof EditorStyleMap)[], K ];
|
|
152
|
+
style: EditorStyleProp<EditorStyleMap[K]>;
|
|
153
|
+
};
|
|
154
|
+
}[keyof EditorStyleMap];
|
|
155
|
+
|
|
149
156
|
export type EditorTheme = {
|
|
150
157
|
[K in keyof EditorStyleMap]?: EditorStyleProp<EditorStyleMap[K]>;
|
|
151
|
-
} & {
|
|
158
|
+
} & {
|
|
159
|
+
rules?: readonly EditorStyleRule[];
|
|
160
|
+
toolbar?: EditorToolbarTheme;
|
|
161
|
+
};
|
|
152
162
|
|
|
153
163
|
export type EditorLinkTheme = EditorInlineStyle;
|
|
154
164
|
export type EditorHeadingTheme = Partial<
|
|
@@ -163,6 +173,10 @@ export type NormalizedEditorStyle = Readonly<Record<string, unknown>>;
|
|
|
163
173
|
export interface NormalizedEditorTheme {
|
|
164
174
|
version: 1;
|
|
165
175
|
styles?: Partial<Record<keyof EditorStyleMap, NormalizedEditorStyle>>;
|
|
176
|
+
rules?: readonly {
|
|
177
|
+
path: readonly (keyof EditorStyleMap)[];
|
|
178
|
+
style: NormalizedEditorStyle;
|
|
179
|
+
}[];
|
|
166
180
|
toolbar?: EditorToolbarTheme;
|
|
167
181
|
}
|
|
168
182
|
|
|
@@ -180,10 +194,41 @@ type ExactStyle<T, Shape> = T extends false | null | undefined
|
|
|
180
194
|
: never;
|
|
181
195
|
};
|
|
182
196
|
|
|
197
|
+
type ExactRuleStyle<T, Shape> = [ EditorStyleProp<Shape> ] extends [ T ]
|
|
198
|
+
? [ T ] extends [ EditorStyleProp<Shape> ]
|
|
199
|
+
? T
|
|
200
|
+
: ExactStyle<T, Shape>
|
|
201
|
+
: ExactStyle<T, Shape>;
|
|
202
|
+
|
|
203
|
+
type ExactEditorStyleRule<T> = T extends {
|
|
204
|
+
readonly path: infer Path extends readonly (keyof EditorStyleMap)[];
|
|
205
|
+
readonly style: unknown;
|
|
206
|
+
}
|
|
207
|
+
? Path extends readonly [ ...(keyof EditorStyleMap)[], infer Target ]
|
|
208
|
+
? Target extends keyof EditorStyleMap
|
|
209
|
+
? {
|
|
210
|
+
[K in keyof T]: K extends 'path'
|
|
211
|
+
? Path
|
|
212
|
+
: K extends 'style'
|
|
213
|
+
? ExactRuleStyle<T[K], EditorStyleMap[Target]>
|
|
214
|
+
: never;
|
|
215
|
+
}
|
|
216
|
+
: never
|
|
217
|
+
: never
|
|
218
|
+
: never;
|
|
219
|
+
|
|
220
|
+
type ExactEditorStyleRules<T> = T extends undefined
|
|
221
|
+
? T
|
|
222
|
+
: T extends readonly unknown[]
|
|
223
|
+
? { [K in keyof T]: ExactEditorStyleRule<T[K]> }
|
|
224
|
+
: never;
|
|
225
|
+
|
|
183
226
|
export type ExactEditorTheme<T> = {
|
|
184
|
-
[K in keyof T]: K extends
|
|
185
|
-
?
|
|
186
|
-
: K extends
|
|
187
|
-
|
|
188
|
-
|
|
227
|
+
[K in keyof T]: K extends 'rules'
|
|
228
|
+
? ExactEditorStyleRules<T[K]>
|
|
229
|
+
: K extends keyof EditorStyleMap
|
|
230
|
+
? ExactStyle<T[K], EditorStyleMap[K]>
|
|
231
|
+
: K extends 'toolbar'
|
|
232
|
+
? EditorToolbarTheme
|
|
233
|
+
: never;
|
|
189
234
|
};
|
package/src/EditorTheme.ts
CHANGED
|
@@ -183,7 +183,7 @@ export function serializeEditorTheme(
|
|
|
183
183
|
const normalized = theme === undefined ? undefined : normalizeEditorTheme(theme);
|
|
184
184
|
|
|
185
185
|
const cleanedTheme =
|
|
186
|
-
normalized && (normalized.styles || normalized.toolbar)
|
|
186
|
+
normalized && (normalized.styles || normalized.rules || normalized.toolbar)
|
|
187
187
|
? stripUndefined(normalized)
|
|
188
188
|
: undefined;
|
|
189
189
|
|
|
@@ -192,6 +192,10 @@ export function serializeEditorTheme(
|
|
|
192
192
|
? (cleanedTheme as Record<string, unknown>)
|
|
193
193
|
: undefined;
|
|
194
194
|
|
|
195
|
+
if (base && normalized?.rules !== undefined) {
|
|
196
|
+
base.rules = normalized.rules;
|
|
197
|
+
}
|
|
198
|
+
|
|
195
199
|
const cleanedMentions = mentionTheme
|
|
196
200
|
? stripUndefined(normalizeEditorMentionTheme(mentionTheme))
|
|
197
201
|
: undefined;
|
package/src/index.ts
CHANGED
|
@@ -359,14 +359,8 @@ export function useRichTextEditorCommands(
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
applyEngineCommand({
|
|
362
|
-
type: '
|
|
362
|
+
type: 'applyListType',
|
|
363
363
|
listType,
|
|
364
|
-
itemType:
|
|
365
|
-
listType === 'taskList'
|
|
366
|
-
? 'taskItem'
|
|
367
|
-
: listType === 'bullet_list' || listType === 'ordered_list'
|
|
368
|
-
? 'list_item'
|
|
369
|
-
: 'listItem',
|
|
370
364
|
});
|
|
371
365
|
},
|
|
372
366
|
[ activeStateRef, applyEngineCommand ]
|