@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.
Files changed (46) hide show
  1. package/android/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +8 -3
  2. package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +5 -1
  3. package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +1 -1
  4. package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +3 -2
  5. package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +196 -8
  6. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +7 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +1 -0
  8. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +48 -9
  9. package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +17 -4
  10. package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +83 -25
  11. package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +28 -14
  12. package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +5 -1
  13. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +5 -1
  14. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +141 -40
  15. package/dist/EditorStyleSheetNormalization.js +37 -0
  16. package/dist/EditorStyleSheetTypes.d.ts +22 -1
  17. package/dist/EditorTheme.js +4 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/useRichTextEditorCommands.js +1 -6
  20. package/ios/EditorCore.xcframework/Info.plist +5 -5
  21. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  22. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  23. package/ios/EditorLayoutManager.swift +2 -4
  24. package/ios/EditorStyleSheet.swift +77 -11
  25. package/ios/EditorTextView+Commands.swift +1 -1
  26. package/ios/EditorTextView+Selection.swift +5 -2
  27. package/ios/EditorTextView.swift +5 -2
  28. package/ios/EditorTheme.swift +3 -1
  29. package/ios/EditorV2Adapter+Commands.swift +10 -0
  30. package/ios/EditorV2Shadow.swift +4 -0
  31. package/ios/RenderBridge+Atoms.swift +15 -6
  32. package/ios/RenderBridge+Style.swift +59 -17
  33. package/ios/RenderBridge.swift +19 -16
  34. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +31 -13
  35. package/ios/Viewer/CoreTextProseLayoutEngine+Models.swift +17 -2
  36. package/ios/Viewer/CoreTextProseLayoutEngine.swift +39 -21
  37. package/package.json +3 -3
  38. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  39. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  40. package/rust/android/x86/libeditor_core.so +0 -0
  41. package/rust/android/x86_64/libeditor_core.so +0 -0
  42. package/src/EditorStyleSheetNormalization.ts +50 -0
  43. package/src/EditorStyleSheetTypes.ts +51 -6
  44. package/src/EditorTheme.ts +5 -1
  45. package/src/index.ts +1 -0
  46. 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 let sheet = theme.styleSheet {
320
- scale = EditorTheme.cgFloat(sheet["listMarker"]["scale"]) ?? (context.ordered ? 1 : LayoutConstants.unorderedListMarkerFontScale)
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: theme.orderedListMarker
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: theme.listMarkerColor.cgColor
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
- let mention = (theme.mention?.merged(with: localMention) ?? localMention)?.node
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
- let checkbox = block.listContext.flatMap { $0.kind == "task" ? sheet?.checkbox(checked: $0.checked) : nil }
239
- let markerGap = checkbox?.number("gap", fallback: 8) ?? theme.listMarkerGap
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: theme.listMarkerColor.cgColor,
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: theme.ruleThickness)
341
- var fragments: [PreparedProseFragment] = [.init(kind: .rule, bounds: rule, color: theme.ruleColor.cgColor, strokeWidth: theme.ruleThickness)]
342
- let totalEnd = y + theme.ruleThickness + theme.ruleMargin + box.inset.bottom
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: theme.listMarkerColor.cgColor, label: marker.label, checked: marker.checked, styleBox: checkbox))
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: theme.listMarkerColor.cgColor, label: marker.label, checked: marker.checked, styleBox: checkbox))
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",
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
@@ -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
- } & { toolbar?: EditorToolbarTheme };
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 keyof EditorStyleMap
185
- ? ExactStyle<T[K], EditorStyleMap[K]>
186
- : K extends 'toolbar'
187
- ? EditorToolbarTheme
188
- : never;
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
  };
@@ -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
@@ -104,6 +104,7 @@ export { serializeEditorTheme } from './EditorTheme';
104
104
  export { EditorStyleSheet } from './EditorStyleSheet';
105
105
  export type {
106
106
  EditorStyleProp,
107
+ EditorStyleRule,
107
108
  EditorStyleMap,
108
109
  EditorTypographyStyle,
109
110
  EditorBorderStyle,
@@ -359,14 +359,8 @@ export function useRichTextEditorCommands(
359
359
  }
360
360
 
361
361
  applyEngineCommand({
362
- type: 'wrapInList',
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 ]