@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.
Files changed (47) hide show
  1. package/README.md +11 -0
  2. package/android/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +8 -3
  3. package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +5 -1
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +1 -1
  5. package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +3 -2
  6. package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +196 -8
  7. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +7 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +1 -0
  9. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +48 -9
  10. package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +17 -4
  11. package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +83 -25
  12. package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +28 -14
  13. package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +5 -1
  14. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +5 -1
  15. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +141 -40
  16. package/dist/EditorStyleSheetNormalization.js +37 -0
  17. package/dist/EditorStyleSheetTypes.d.ts +22 -1
  18. package/dist/EditorTheme.js +4 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/useRichTextEditorCommands.js +1 -6
  21. package/ios/EditorCore.xcframework/Info.plist +5 -5
  22. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +2 -2
  23. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +2 -2
  24. package/ios/EditorLayoutManager.swift +2 -4
  25. package/ios/EditorStyleSheet.swift +77 -11
  26. package/ios/EditorTextView+Commands.swift +1 -1
  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/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.mm +1 -0
  38. package/package.json +3 -3
  39. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  40. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  41. package/rust/android/x86/libeditor_core.so +0 -0
  42. package/rust/android/x86_64/libeditor_core.so +0 -0
  43. package/src/EditorStyleSheetNormalization.ts +50 -0
  44. package/src/EditorStyleSheetTypes.ts +51 -6
  45. package/src/EditorTheme.ts +5 -1
  46. package/src/index.ts +1 -0
  47. package/src/useRichTextEditorCommands.tsx +1 -7
package/README.md CHANGED
@@ -18,6 +18,17 @@ See the [documentation](https://github.com/apollohg/react-native-rich-text-edito
18
18
  **⭐ Optional native code syntax highlighting**
19
19
  **⭐ Rich copy and paste**
20
20
 
21
+ ## Roadmap
22
+
23
+ Below is a list of features that are planned or in progress. If you have a feature/idea suggestion, please [add it to the discussion](https://github.com/apollohg/react-native-rich-text-editor/discussions/4).
24
+
25
+ - [x] External text composition
26
+ - [x] Custom atoms via React Native components
27
+ - [ ] Tables *(in progress)*
28
+ - [ ] Output as Markdown/GitHub Flavored Markdown (GFM)
29
+ - [ ] Extension framework
30
+ - [ ] Inline node extensions
31
+
21
32
  ## Requirements
22
33
 
23
34
  The package uses custom native code and Expo Modules. Use a development build or a bare React Native app with Expo Modules configured; it does not run in Expo Go.
@@ -6,7 +6,12 @@ import android.graphics.Path
6
6
  import android.graphics.RectF
7
7
  import android.text.style.ReplacementSpan
8
8
 
9
- internal fun resolvedCheckboxStyle(sheet: EditorStyleSheet, checked: Boolean): EditorElementStyle {
9
+ internal fun resolvedCheckboxStyle(
10
+ sheet: EditorStyleSheet,
11
+ checked: Boolean,
12
+ ancestors: List<String> = emptyList()
13
+ ): EditorElementStyle {
14
+ val resolved = sheet.resolveElement("taskCheckbox", ancestors)
10
15
  val base = EditorElementStyle(
11
16
  EditorTextStyle(),
12
17
  EditorBoxStyle(
@@ -16,8 +21,8 @@ internal fun resolvedCheckboxStyle(sheet: EditorStyleSheet, checked: Boolean): E
16
21
  size = 18f,
17
22
  gap = 6f
18
23
  )
19
- .mergedWith(sheet["taskCheckbox"])
20
- return if (checked) base.mergedWith(sheet["taskCheckbox"]?.checked) else base
24
+ .mergedWith(resolved)
25
+ return if (checked) base.mergedWith(resolved?.checked) else base
21
26
  }
22
27
 
23
28
  internal fun drawCheckbox(
@@ -75,7 +75,11 @@ internal fun EditorEditText.applyThemeImpl(theme: EditorTheme?) {
75
75
  background =
76
76
  EditorBoxDrawable(it.box("content").scaled(resources.displayMetrics.density))
77
77
  }
78
- applyContentInsets(theme?.contentInsets)
78
+ val content = theme?.styleSheet?.box("content", emptyList())?.outerInset
79
+ applyContentInsets(
80
+ content?.let { EditorContentInsets(it.top, it.right, it.bottom, it.left) }
81
+ ?: theme?.contentInsets
82
+ )
79
83
  if (hasLiveEditor()) {
80
84
  val previousScrollX = scrollX
81
85
  val previousScrollY = scrollY
@@ -22,7 +22,7 @@ internal fun EditorEditText.performToolbarToggleListImpl(listType: String, isAct
22
22
  val update = if (isActive) {
23
23
  driver.unwrapFromList(selection.first, selection.second)
24
24
  } else {
25
- driver.wrapInList(listType, selection.first, selection.second)
25
+ driver.applyListType(listType, selection.first, selection.second)
26
26
  }
27
27
  update?.let { applyUpdateJSON(it) }
28
28
  }
@@ -81,7 +81,8 @@ internal fun EditorElementStyle.mergedWith(other: EditorElementStyle?): EditorEl
81
81
  internal fun resolvedMentionStyle(
82
82
  base: EditorTextStyle,
83
83
  theme: EditorTheme?,
84
- local: EditorMentionTheme?
84
+ local: EditorMentionTheme?,
85
+ ancestors: List<String> = emptyList()
85
86
  ): EditorElementStyle {
86
87
  var result = EditorElementStyle(
87
88
  base,
@@ -91,7 +92,7 @@ internal fun resolvedMentionStyle(
91
92
  corners = EditorCorners(6f, 6f, 6f, 6f)
92
93
  )
93
94
  )
94
- .mergedWith(theme?.styleSheet?.get("mention"))
95
+ .mergedWith(theme?.styleSheet?.resolveElement("mention", ancestors))
95
96
  listOf(theme?.mentions?.node, local?.node).forEach { node ->
96
97
  if (node != null) {
97
98
  result = result.copy(
@@ -71,13 +71,32 @@ data class EditorElementStyle(
71
71
  val declaredProperties: Set<String> = emptySet()
72
72
  )
73
73
 
74
- class EditorStyleSheet private constructor(val styles: Map<String, EditorElementStyle>) {
74
+ private data class EditorStyleRule(val path: List<String>, val style: JSONObject)
75
+
76
+ class EditorStyleSheet private constructor(
77
+ val styles: Map<String, EditorElementStyle>,
78
+ private val rules: List<EditorStyleRule> = emptyList()
79
+ ) {
75
80
  operator fun get(element: String): EditorElementStyle? = styles[canonicalElement(element)]
76
81
 
77
- fun resolveText(
82
+ fun resolveElement(
78
83
  element: String,
79
- ancestors: List<String> = emptyList(),
80
- marks: List<String> = emptyList()
84
+ ancestors: List<String> = emptyList()
85
+ ): EditorElementStyle? {
86
+ val name = canonicalMark(canonicalElement(element))
87
+ val matches = matchingRules(name, ancestors)
88
+ val existing = this[name]
89
+ if (matches.isEmpty()) return existing
90
+ var result = existing ?: decodeElement(JSONObject(), defaultBox(name))
91
+ for (rule in matches) {
92
+ result = result.overlaidWith(decodeElement(rule.style, result.box), rule.style)
93
+ }
94
+ return result
95
+ }
96
+
97
+ internal fun resolveBaseText(
98
+ element: String,
99
+ ancestors: List<String> = emptyList()
81
100
  ): EditorTextStyle {
82
101
  val name = canonicalElement(element)
83
102
  var result = EditorTextStyle().mergedWith(this["text"]?.text)
@@ -85,18 +104,54 @@ class EditorStyleSheet private constructor(val styles: Map<String, EditorElement
85
104
  ancestors.forEach {
86
105
  result = result.mergedWith(this[it]?.text?.copy(backgroundColor = null))
87
106
  }
88
- result = result.mergedWith(this[name]?.text?.copy(backgroundColor = null))
107
+ return result.mergedWith(this[name]?.text?.copy(backgroundColor = null))
108
+ }
109
+
110
+ fun resolveText(
111
+ element: String,
112
+ ancestors: List<String> = emptyList(),
113
+ marks: List<String> = emptyList()
114
+ ): EditorTextStyle {
115
+ val name = canonicalElement(element)
116
+ var result = resolveBaseText(name, ancestors)
117
+ matchingRules(name, ancestors).forEach {
118
+ result =
119
+ result.mergedWith(EditorTextStyle.fromJson(it.style)?.copy(backgroundColor = null))
120
+ }
89
121
  val active = marks.map(::canonicalMark).toSet()
90
122
  listOf("inlineCode", "bold", "italic", "link", "underline", "strike").filter {
91
123
  it in active
92
124
  }.forEach {
93
125
  result = result.mergedWith(semanticText(it)).mergedWith(this[it]?.text)
126
+ matchingRules(it, ancestors + name).forEach { rule ->
127
+ result = result.mergedWith(EditorTextStyle.fromJson(rule.style))
128
+ }
94
129
  }
95
130
  return result
96
131
  }
97
132
 
98
- fun box(element: String): EditorBoxStyle =
99
- this[element]?.box ?: defaultBox(canonicalElement(element))
133
+ fun box(element: String): EditorBoxStyle = box(element, emptyList())
134
+
135
+ fun box(element: String, ancestors: List<String>): EditorBoxStyle {
136
+ var result = this[element]?.box ?: defaultBox(canonicalElement(element))
137
+ for (rule in matchingRules(element, ancestors)) {
138
+ result = decodeElement(rule.style, result).box
139
+ }
140
+ return result
141
+ }
142
+
143
+ internal fun hasMatchingRuleProperty(
144
+ element: String,
145
+ ancestors: List<String>,
146
+ property: String
147
+ ): Boolean = matchingRules(element, ancestors).any { it.style.has(property) }
148
+
149
+ private fun matchingRules(element: String, ancestors: List<String>): List<EditorStyleRule> {
150
+ val chain = (ancestors + element).map { canonicalMark(canonicalElement(it)) }
151
+ return rules.filter { rule ->
152
+ rule.path.size <= chain.size && chain.takeLast(rule.path.size) == rule.path
153
+ }
154
+ }
100
155
 
101
156
  companion object {
102
157
  internal fun decodeTheme(root: JSONObject): EditorTheme? {
@@ -110,7 +165,8 @@ class EditorStyleSheet private constructor(val styles: Map<String, EditorElement
110
165
  val sheet = EditorStyleSheet(
111
166
  values.keys().asSequence().associateWith { name ->
112
167
  decodeElement(values.getJSONObject(name), defaultBox(name))
113
- }
168
+ },
169
+ decodeRules(root)
114
170
  )
115
171
  val content = sheet.box("content").outerInset
116
172
  val marker = sheet["listMarker"]
@@ -160,6 +216,27 @@ class EditorStyleSheet private constructor(val styles: Map<String, EditorElement
160
216
  )
161
217
  }
162
218
 
219
+ private fun decodeRules(root: JSONObject): List<EditorStyleRule> {
220
+ val values = root.optJSONArray("rules") ?: return emptyList()
221
+ return buildList {
222
+ for (index in 0 until values.length()) {
223
+ val entry = values.optJSONObject(index) ?: continue
224
+ val pathValues = entry.optJSONArray("path") ?: continue
225
+ if (pathValues.length() == 0) continue
226
+ val path = mutableListOf<String>()
227
+ for (pathIndex in 0 until pathValues.length()) {
228
+ val raw = pathValues.opt(pathIndex) as? String ?: break
229
+ val name = canonicalMark(canonicalElement(raw))
230
+ if (name !in STYLE_NAMES) break
231
+ path.add(name)
232
+ }
233
+ if (path.size != pathValues.length()) continue
234
+ val style = entry.optJSONObject("style") ?: continue
235
+ add(EditorStyleRule(path.toList(), style))
236
+ }
237
+ }
238
+ }
239
+
163
240
  internal fun decodeElement(
164
241
  json: JSONObject,
165
242
  fallback: EditorBoxStyle = EditorBoxStyle()
@@ -211,6 +288,117 @@ class EditorStyleSheet private constructor(val styles: Map<String, EditorElement
211
288
  }
212
289
  }
213
290
 
291
+ private val STYLE_NAMES = setOf(
292
+ "content", "text", "paragraph", "h1", "h2", "h3", "h4", "h5", "h6",
293
+ "blockquote", "codeBlock", "bulletList", "orderedList", "taskList", "listItem",
294
+ "taskItem", "listMarker", "taskCheckbox", "horizontalRule", "image", "link",
295
+ "inlineCode", "bold", "italic", "underline", "strike", "mention", "placeholder"
296
+ )
297
+
298
+ private fun EditorElementStyle.overlaidWith(
299
+ other: EditorElementStyle,
300
+ raw: JSONObject? = null
301
+ ): EditorElementStyle {
302
+ val keys = other.declaredProperties
303
+ fun sides(prefix: String, current: EditorEdges, next: EditorEdges, suffix: String = "") =
304
+ EditorEdges(
305
+ if ("${prefix}Top$suffix" in keys) next.top else current.top,
306
+ if ("${prefix}Right$suffix" in keys) next.right else current.right,
307
+ if ("${prefix}Bottom$suffix" in keys) next.bottom else current.bottom,
308
+ if ("${prefix}Left$suffix" in keys) next.left else current.left
309
+ )
310
+ val nextChecked = if ("checked" in keys) {
311
+ other.checked?.let {
312
+ (checked ?: EditorElementStyle(EditorTextStyle(), box)).overlaidWith(
313
+ it,
314
+ raw?.optJSONObject("checked")
315
+ )
316
+ }
317
+ } else {
318
+ checked
319
+ }
320
+ val nextOrdered = if ("ordered" in keys) {
321
+ other.ordered?.let { next ->
322
+ val current = ordered ?: EditorOrderedListMarkerTheme()
323
+ val orderedJson = raw?.optJSONObject("ordered")
324
+ EditorOrderedListMarkerTheme(
325
+ schemes = if (orderedJson?.has("schemes") ==
326
+ true
327
+ ) {
328
+ next.schemes
329
+ } else {
330
+ current.schemes
331
+ },
332
+ suffix = if (orderedJson?.has("suffix") == true) next.suffix else current.suffix
333
+ )
334
+ }
335
+ } else {
336
+ ordered
337
+ }
338
+ return copy(
339
+ text = text.mergedWith(other.text),
340
+ box = box.copy(
341
+ backgroundColor = if ("backgroundColor" in keys) {
342
+ other.box.backgroundColor
343
+ } else {
344
+ box.backgroundColor
345
+ },
346
+ padding = sides("padding", box.padding, other.box.padding),
347
+ margin = sides("margin", box.margin, other.box.margin),
348
+ border = sides("border", box.border, other.box.border, "Width"),
349
+ borderColors = listOf("Top", "Right", "Bottom", "Left").mapIndexed { index, side ->
350
+ if ("border${side}Color" in keys) {
351
+ other.box.borderColors[index]
352
+ } else {
353
+ box.borderColors[index]
354
+ }
355
+ },
356
+ corners = EditorCorners(
357
+ if ("borderTopLeftRadius" in
358
+ keys
359
+ ) {
360
+ other.box.corners.topLeft
361
+ } else {
362
+ box.corners.topLeft
363
+ },
364
+ if ("borderTopRightRadius" in
365
+ keys
366
+ ) {
367
+ other.box.corners.topRight
368
+ } else {
369
+ box.corners.topRight
370
+ },
371
+ if ("borderBottomRightRadius" in keys) {
372
+ other.box.corners.bottomRight
373
+ } else {
374
+ box.corners.bottomRight
375
+ },
376
+ if ("borderBottomLeftRadius" in keys) {
377
+ other.box.corners.bottomLeft
378
+ } else {
379
+ box.corners.bottomLeft
380
+ }
381
+ ),
382
+ borderStyle = if ("borderStyle" in keys) other.box.borderStyle else box.borderStyle
383
+ ),
384
+ indent = if ("indent" in keys) other.indent else indent,
385
+ baseIndentMultiplier = if ("baseIndentMultiplier" in keys) {
386
+ other.baseIndentMultiplier
387
+ } else {
388
+ baseIndentMultiplier
389
+ },
390
+ scale = if ("scale" in keys) other.scale else scale,
391
+ gap = if ("gap" in keys) other.gap else gap,
392
+ ordered = nextOrdered,
393
+ checked = nextChecked,
394
+ resizeMode = if ("resizeMode" in keys) other.resizeMode else resizeMode,
395
+ size = if ("size" in keys) other.size else size,
396
+ checkColor = if ("checkColor" in keys) other.checkColor else checkColor,
397
+ height = if ("height" in keys) other.height else height,
398
+ declaredProperties = declaredProperties + keys
399
+ )
400
+ }
401
+
214
402
  internal fun canonicalElement(name: String): String = when (name) {
215
403
  "bullet_list" -> "bulletList"
216
404
  "ordered_list" -> "orderedList"
@@ -870,6 +870,13 @@ internal class EditorV2Adapter private constructor(
870
870
  override fun toggleBlockquote(anchor: Int, head: Int): String? =
871
871
  commandAtSelection(JSONObject().put("type", "toggleBlockquote"), anchor, head)
872
872
 
873
+ override fun applyListType(listType: String, anchor: Int, head: Int): String? =
874
+ commandAtSelection(
875
+ JSONObject().put("type", "applyListType").put("listType", listType),
876
+ anchor,
877
+ head
878
+ )
879
+
873
880
  override fun wrapInList(listType: String, anchor: Int, head: Int): String? {
874
881
  val itemType = EditorNodeTypes.listItemType(listType)
875
882
  return commandAtSelection(
@@ -66,6 +66,7 @@ internal interface EditorV2Driver {
66
66
  fun toggleHeading(level: Int, anchor: Int, head: Int): String?
67
67
  fun toggleCodeBlock(anchor: Int, head: Int): String?
68
68
  fun toggleBlockquote(anchor: Int, head: Int): String?
69
+ fun applyListType(listType: String, anchor: Int, head: Int): String?
69
70
  fun wrapInList(listType: String, anchor: Int, head: Int): String?
70
71
  fun unwrapFromList(anchor: Int, head: Int): String?
71
72
  fun indentListItem(anchor: Int, head: Int): String?
@@ -83,7 +83,9 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
83
83
  val selectedSpan = editorView?.selectedImageSpanForResize()
84
84
  if (currentGeometry?.docPos != nextGeometry.docPos ||
85
85
  dragState?.span?.let { it !== selectedSpan } == true
86
- ) cancelActiveResize()
86
+ ) {
87
+ cancelActiveResize()
88
+ }
87
89
  currentGeometry = nextGeometry
88
90
  visibility = VISIBLE
89
91
  bringToFront()
@@ -138,7 +140,10 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
138
140
  fixedHeightPx = geometry.rect.height() - span.currentSizePx().second,
139
141
  maximumWidthPx = editorView?.maximumImageWidthPx() ?: geometry.rect.width(),
140
142
  previewRect = RectF(geometry.rect),
141
- previewContentSize = span.currentSizePx().let { it.first.toFloat() to it.second.toFloat() }
143
+ previewContentSize = span.currentSizePx().let {
144
+ it.first.toFloat() to
145
+ it.second.toFloat()
146
+ }
142
147
  )
143
148
  parent?.requestDisallowInterceptTouchEvent(true)
144
149
  return true
@@ -163,7 +168,9 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
163
168
  )
164
169
  }
165
170
  editorView?.restoreImageResizePreview(state.span)
166
- } else editorView?.restoreImageResizePreview(state.span)
171
+ } else {
172
+ editorView?.restoreImageResizePreview(state.span)
173
+ }
167
174
  dragState = null
168
175
  parent?.requestDisallowInterceptTouchEvent(false)
169
176
  post { refresh() }
@@ -188,7 +195,9 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
188
195
  originalContentSize.first,
189
196
  originalContentSize.second
190
197
  )
191
- ) return
198
+ ) {
199
+ return
200
+ }
192
201
  state.previewRect = RectF(state.originalRect)
193
202
  state.previewContentSize = originalContentSize
194
203
  currentGeometry = boundEditor.selectedImageGeometry()
@@ -209,7 +218,14 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
209
218
  maximumWidthPx = state.maximumWidthPx
210
219
  )
211
220
  val boundEditor = editorView ?: return
212
- if (!boundEditor.previewImageResize(state.span, contentSize.first, contentSize.second)) return
221
+ if (!boundEditor.previewImageResize(
222
+ state.span,
223
+ contentSize.first,
224
+ contentSize.second
225
+ )
226
+ ) {
227
+ return
228
+ }
213
229
  state.previewRect = RectF(nextRect)
214
230
  state.previewContentSize = contentSize
215
231
  currentGeometry = boundEditor.selectedImageGeometry()
@@ -324,10 +340,33 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
324
340
  val visualWidth = width + fixedWidthPx
325
341
  val visualHeight = height + fixedHeightPx
326
342
  val rect = when (corner) {
327
- Corner.TOP_LEFT -> RectF(anchor.x - visualWidth, anchor.y - visualHeight, anchor.x, anchor.y)
328
- Corner.TOP_RIGHT -> RectF(anchor.x, anchor.y - visualHeight, anchor.x + visualWidth, anchor.y)
329
- Corner.BOTTOM_LEFT -> RectF(anchor.x - visualWidth, anchor.y, anchor.x, anchor.y + visualHeight)
330
- Corner.BOTTOM_RIGHT -> RectF(anchor.x, anchor.y, anchor.x + visualWidth, anchor.y + visualHeight)
343
+ Corner.TOP_LEFT -> RectF(
344
+ anchor.x - visualWidth,
345
+ anchor.y - visualHeight,
346
+ anchor.x,
347
+ anchor.y
348
+ )
349
+
350
+ Corner.TOP_RIGHT -> RectF(
351
+ anchor.x,
352
+ anchor.y - visualHeight,
353
+ anchor.x + visualWidth,
354
+ anchor.y
355
+ )
356
+
357
+ Corner.BOTTOM_LEFT -> RectF(
358
+ anchor.x - visualWidth,
359
+ anchor.y,
360
+ anchor.x,
361
+ anchor.y + visualHeight
362
+ )
363
+
364
+ Corner.BOTTOM_RIGHT -> RectF(
365
+ anchor.x,
366
+ anchor.y,
367
+ anchor.x + visualWidth,
368
+ anchor.y + visualHeight
369
+ )
331
370
  }
332
371
  return rect to (width to height)
333
372
  }
@@ -285,9 +285,9 @@ internal fun RenderBridge.calculateIndent(
285
285
  density: Float
286
286
  ): Float {
287
287
  theme?.styleSheet?.let { sheet ->
288
- val lists = blockStack.filter { it.listContext != null }
289
- return lists.mapIndexed { index, block ->
290
- val context = block.listContext!!
288
+ val lists = blockStack.withIndex().filter { it.value.listContext != null }
289
+ return lists.mapIndexed { index, entry ->
290
+ val context = entry.value.listContext!!
291
291
  val name = if (context.optString("kind") ==
292
292
  "task"
293
293
  ) {
@@ -297,7 +297,20 @@ internal fun RenderBridge.calculateIndent(
297
297
  } else {
298
298
  "bulletList"
299
299
  }
300
- val style = sheet[name]
300
+ val prefix = blockStack.take(entry.index)
301
+ val containerIndex = prefix.indexOfLast { canonicalElement(it.nodeType) == name }
302
+ val style = sheet.resolveElement(
303
+ name,
304
+ prefix.take(
305
+ if (containerIndex >=
306
+ 0
307
+ ) {
308
+ containerIndex
309
+ } else {
310
+ prefix.size
311
+ }
312
+ ).map { it.nodeType }
313
+ )
301
314
  (style?.indent ?: LayoutConstants.INDENT_PER_DEPTH) *
302
315
  (if (index == 0) style?.baseIndentMultiplier ?: 1f else 1f) *
303
316
  density