Skip to content

Commit 57a8982

Browse files
authored
feat: optimize task list item handling (#100)
* feat: optimize task list item handling and enhance markdown rendering * refactor: remove outdated comments and optimize task list item handling
1 parent b992065 commit 57a8982

8 files changed

Lines changed: 445 additions & 64 deletions

File tree

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,10 @@ class EnrichedMarkdownManager :
5252
}
5353

5454
view?.setOnTaskListItemPressCallback { taskIndex, checked, itemText ->
55-
val updatedMarkdown =
56-
TaskListToggleUtils.toggleAtIndex(
57-
view.currentMarkdown,
58-
taskIndex,
59-
checked,
60-
)
55+
val newChecked = !checked
56+
val updatedMarkdown = TaskListToggleUtils.toggleAtIndex(view.currentMarkdown, taskIndex, newChecked)
6157
view.setMarkdownContent(updatedMarkdown)
62-
emitOnTaskListItemPress(view, taskIndex, !checked, itemText)
58+
emitOnTaskListItemPress(view, taskIndex, newChecked, itemText)
6359
}
6460

6561
view?.setMarkdownContent(markdown ?: "")

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.swmansion.enriched.markdown.events.LinkLongPressEvent
1515
import com.swmansion.enriched.markdown.events.LinkPressEvent
1616
import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
1717
import com.swmansion.enriched.markdown.parser.Md4cFlags
18+
import com.swmansion.enriched.markdown.utils.TaskListTapUtils
1819
import com.swmansion.enriched.markdown.utils.TaskListToggleUtils
1920

2021
@ReactModule(name = EnrichedMarkdownTextManager.NAME)
@@ -58,14 +59,22 @@ class EnrichedMarkdownTextManager :
5859
}
5960

6061
view?.setOnTaskListItemPressCallback { taskIndex, checked, itemText ->
61-
val updatedMarkdown =
62-
TaskListToggleUtils.toggleAtIndex(
63-
view.currentMarkdown,
64-
taskIndex,
65-
checked,
66-
)
62+
val newChecked = !checked
63+
64+
val styleConfig = view.markdownStyle
65+
val optimizedSuccess =
66+
styleConfig != null && TaskListTapUtils.updateTaskListItemCheckedState(view, taskIndex, newChecked, styleConfig)
67+
68+
if (optimizedSuccess) {
69+
emitOnTaskListItemPress(view, taskIndex, newChecked, itemText)
70+
return@setOnTaskListItemPressCallback
71+
}
72+
73+
val currentMarkdown = view.currentMarkdown
74+
val updatedMarkdown = TaskListToggleUtils.toggleAtIndex(currentMarkdown, taskIndex, newChecked)
6775
view.setMarkdownContent(updatedMarkdown)
68-
emitOnTaskListItemPress(view, taskIndex, !checked, itemText)
76+
77+
emitOnTaskListItemPress(view, taskIndex, newChecked, itemText)
6978
}
7079

7180
view?.setMarkdownContent(markdown ?: "No markdown content")

android/src/main/java/com/swmansion/enriched/markdown/utils/TaskListTapUtils.kt

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
package com.swmansion.enriched.markdown.utils
22

3+
import android.text.Spannable
4+
import android.text.SpannableStringBuilder
35
import android.text.Spanned
6+
import android.text.style.ForegroundColorSpan
7+
import android.text.style.StrikethroughSpan
48
import android.widget.TextView
9+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
10+
import com.swmansion.enriched.markdown.spans.BaseListSpan
511
import com.swmansion.enriched.markdown.spans.TaskListSpan
12+
import com.swmansion.enriched.markdown.styles.StyleConfig
13+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
614

715
data class TaskListHitTestResult(
816
val taskIndex: Int,
@@ -23,7 +31,8 @@ object TaskListToggleUtils {
2331

2432
val match = matches[index]
2533
val prefix = match.groupValues[1]
26-
val replacement = "$prefix[${if (checked) " " else "x"}]"
34+
35+
val replacement = "$prefix[${if (checked) "x" else " "}]"
2736

2837
return markdown.replaceRange(match.range, replacement)
2938
}
@@ -79,4 +88,162 @@ object TaskListTapUtils {
7988
itemText = itemText,
8089
)
8190
}
91+
92+
fun updateTaskListItemCheckedState(
93+
textView: TextView,
94+
targetIndex: Int,
95+
newChecked: Boolean,
96+
styleConfig: StyleConfig,
97+
): Boolean {
98+
val text = textView.text
99+
if (text !is Spannable) {
100+
return false
101+
}
102+
103+
val spannable = SpannableStringBuilder(text)
104+
val taskSpans = spannable.getSpans(0, spannable.length, TaskListSpan::class.java)
105+
106+
val targetSpan = taskSpans.firstOrNull { it.taskIndex == targetIndex }
107+
if (targetSpan == null) {
108+
return false
109+
}
110+
111+
if (targetSpan.isChecked == newChecked) {
112+
return true
113+
}
114+
115+
val spanStart = spannable.getSpanStart(targetSpan)
116+
val spanEnd = spannable.getSpanEnd(targetSpan)
117+
val itemDepth = targetSpan.depth
118+
119+
val styleCache = SpanStyleCache(styleConfig)
120+
val newTaskSpan =
121+
TaskListSpan(
122+
taskStyle = styleConfig.taskListStyle,
123+
listStyle = styleConfig.listStyle,
124+
depth = itemDepth,
125+
context = textView.context,
126+
styleCache = styleCache,
127+
taskIndex = targetIndex,
128+
isChecked = newChecked,
129+
)
130+
131+
spannable.removeSpan(targetSpan)
132+
spannable.setSpan(newTaskSpan, spanStart, spanEnd, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
133+
134+
val verifySpans = spannable.getSpans(spanStart, spanEnd, TaskListSpan::class.java)
135+
val verifySpan = verifySpans.firstOrNull { it.taskIndex == targetIndex }
136+
if (verifySpan == null || verifySpan.isChecked != newChecked) {
137+
return false
138+
}
139+
140+
val taskStyle = styleConfig.taskListStyle
141+
val checkedTextColor = taskStyle.checkedTextColor
142+
val strikethrough = taskStyle.checkedStrikethrough
143+
144+
val excludedRanges =
145+
spannable
146+
.getSpans(spanStart, spanEnd, BaseListSpan::class.java)
147+
.filter { it.depth > itemDepth }
148+
.map { spannable.getSpanStart(it) to spannable.getSpanEnd(it) }
149+
.sortedBy { it.first }
150+
151+
applyDecorationsToRanges(
152+
spannable = spannable,
153+
spanStart = spanStart,
154+
spanEnd = spanEnd,
155+
excludedRanges = excludedRanges,
156+
isChecked = newChecked,
157+
checkedTextColor = checkedTextColor,
158+
strikethrough = strikethrough,
159+
styleConfig = styleConfig,
160+
)
161+
162+
val imageSpans = text.getSpans(0, text.length, com.swmansion.enriched.markdown.spans.ImageSpan::class.java).toList()
163+
val originalSpanStarts = imageSpans.associateWith { text.getSpanStart(it) }
164+
165+
textView.text = spannable
166+
167+
val newImageSpans = spannable.getSpans(0, spannable.length, com.swmansion.enriched.markdown.spans.ImageSpan::class.java)
168+
imageSpans.forEach { originalSpan ->
169+
val matchingSpan =
170+
newImageSpans.firstOrNull {
171+
it.imageUrl == originalSpan.imageUrl &&
172+
spannable.getSpanStart(it) == originalSpanStarts[originalSpan]
173+
}
174+
(matchingSpan ?: originalSpan).registerTextView(textView)
175+
}
176+
177+
textView.invalidate()
178+
179+
return true
180+
}
181+
182+
private fun applyDecorationsToRanges(
183+
spannable: Spannable,
184+
spanStart: Int,
185+
spanEnd: Int,
186+
excludedRanges: List<Pair<Int, Int>>,
187+
isChecked: Boolean,
188+
checkedTextColor: Int,
189+
strikethrough: Boolean,
190+
styleConfig: StyleConfig,
191+
) {
192+
var currentPos = spanStart
193+
for ((start, end) in excludedRanges) {
194+
if (start > currentPos) {
195+
if (isChecked) {
196+
applyCheckedSpans(spannable, currentPos, start, checkedTextColor, strikethrough)
197+
} else {
198+
removeCheckedSpans(spannable, currentPos, start, styleConfig)
199+
}
200+
}
201+
currentPos = maxOf(currentPos, end)
202+
}
203+
if (currentPos < spanEnd) {
204+
if (isChecked) {
205+
applyCheckedSpans(spannable, currentPos, spanEnd, checkedTextColor, strikethrough)
206+
} else {
207+
removeCheckedSpans(spannable, currentPos, spanEnd, styleConfig)
208+
}
209+
}
210+
}
211+
212+
private fun applyCheckedSpans(
213+
spannable: Spannable,
214+
start: Int,
215+
end: Int,
216+
color: Int,
217+
strikethrough: Boolean,
218+
) {
219+
if (color != 0) {
220+
spannable.setSpan(ForegroundColorSpan(color), start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
221+
}
222+
if (strikethrough) {
223+
spannable.setSpan(StrikethroughSpan(), start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
224+
}
225+
}
226+
227+
private fun removeCheckedSpans(
228+
spannable: Spannable,
229+
start: Int,
230+
end: Int,
231+
styleConfig: StyleConfig,
232+
) {
233+
val strikethroughSpans = spannable.getSpans(start, end, StrikethroughSpan::class.java)
234+
strikethroughSpans.forEach { spannable.removeSpan(it) }
235+
236+
val colorSpans = spannable.getSpans(start, end, ForegroundColorSpan::class.java)
237+
colorSpans.forEach { spannable.removeSpan(it) }
238+
239+
val listStyleColor = styleConfig.listStyle.color
240+
if (listStyleColor != 0) {
241+
spannable.setSpan(
242+
ForegroundColorSpan(listStyleColor),
243+
start,
244+
end,
245+
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
246+
)
247+
}
248+
}
82249
}

example/src/sampleMarkdown.ts

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,39 @@ The largest terrestrial biome, spanning across **Northern Russia, Canada, and Sc
9898
9999
---
100100
101+
## Forest Statistics by Type
102+
103+
| Forest Type | Coverage | Annual Rainfall | Biodiversity | Carbon Storage |
104+
|------------|----------|-----------------|--------------|----------------|
105+
| Tropical Rainforest | ~7% of land | 80-400 inches | Highest (50%+ species) | High |
106+
| Temperate Forest | ~16% of land | 30-60 inches | Moderate | Moderate |
107+
| Boreal Forest (Taiga) | ~11% of land | 15-40 inches | Lower | Highest |
108+
| Mediterranean Forest | ~2% of land | 20-40 inches | Moderate | Moderate |
109+
110+
---
111+
101112
## Fascinating Forest Facts
102113
103114
Did you know that trees communicate through an underground network? Scientists call this the \`Wood Wide Web\` — a fungal network connecting tree roots across entire forests.
104115
116+
\`\`\`javascript
117+
// Example: Tree network simulation
118+
class TreeNetwork {
119+
constructor() {
120+
this.trees = [];
121+
this.fungalConnections = new Map();
122+
}
123+
124+
connectTrees(tree1, tree2) {
125+
// Trees share nutrients through mycorrhizal networks
126+
this.fungalConnections.set(\`\${tree1.id}-\${tree2.id}\`, {
127+
nutrientFlow: 'bidirectional',
128+
signalTransmission: true
129+
});
130+
}
131+
}
132+
\`\`\`
133+
105134
> Trees share nutrients and information through mycorrhizal networks, essentially "talking" to each other underground.
106135
107136
### The Science of Tree Communication
@@ -209,14 +238,38 @@ Non-native species disrupt forest ecosystems:
209238
210239
Conservation efforts are critical for preserving these vital ecosystems. Here's how you can help:
211240
212-
1. Support *sustainable forestry* practices
213-
2. Reduce paper consumption and **recycle**
214-
3. Plant native trees in your community
215-
4. Donate to ***conservation organizations***
216-
5. Choose products with \`FSC certification\`
217-
6. Advocate for *protected areas*
218-
7. Reduce your **carbon footprint**
219-
8. Educate others about forest importance
241+
### Action Checklist
242+
243+
- [x] Support *sustainable forestry* practices
244+
- [x] Research certified sustainable wood products
245+
- [ ] Learn about forest management practices
246+
- [x] Reduce paper consumption and **recycle**
247+
- [x] Switch to digital documents when possible
248+
- [ ] Set up paper recycling at home
249+
- [ ] Plant native trees in your community
250+
- [ ] Join local tree planting events
251+
- [ ] Choose appropriate native species
252+
- [ ] Donate to ***conservation organizations***
253+
- [ ] Choose products with \`FSC certification\`
254+
- [ ] Look for FSC logo on products
255+
- [ ] Support companies with sustainable practices
256+
- [ ] Advocate for *protected areas*
257+
- [ ] Reduce your **carbon footprint**
258+
- [ ] Use public transportation
259+
- [ ] Reduce energy consumption
260+
- [ ] Educate others about forest importance
261+
262+
### Priority Actions
263+
264+
1. ~~Ignore the problem~~ → **Take immediate action**
265+
2. Support *sustainable forestry* practices
266+
3. Reduce paper consumption and **recycle**
267+
4. Plant native trees in your community
268+
5. Donate to ***conservation organizations***
269+
6. Choose products with \`FSC certification\`
270+
7. Advocate for *protected areas*
271+
8. Reduce your **carbon footprint**
272+
9. Educate others about forest importance
220273
221274
> The best time to plant a tree was 20 years ago. The second best time is now.
222275
>
@@ -233,10 +286,12 @@ Conservation efforts are making a difference:
233286
234287
### Organizations Making an Impact
235288
236-
- **World Wildlife Fund** — global conservation
237-
- *Rainforest Alliance* — sustainable agriculture
238-
- ***The Nature Conservancy*** — land protection
239-
- One Tree Planted — reforestation projects
289+
| Organization | Focus Area | Impact |
290+
|--------------|------------|--------|
291+
| **World Wildlife Fund** | Global conservation | Protected millions of hectares |
292+
| *Rainforest Alliance* | Sustainable agriculture | Certified 5M+ hectares |
293+
| ***The Nature Conservancy*** | Land protection | Protected 125M+ acres |
294+
| One Tree Planted | Reforestation | Planted 100M+ trees |
240295
241296
---
242297
@@ -251,6 +306,22 @@ Scientists and conservationists are developing innovative approaches to protect
251306
- ***DNA barcoding*** to track illegal logging
252307
- AI-powered \`fire prediction\` systems
253308
309+
\`\`\`python
310+
# Example: Forest monitoring with satellite data
311+
import satellite_imagery
312+
313+
def detect_deforestation(region):
314+
"""Monitor forest cover changes using satellite imagery"""
315+
current_cover = satellite_imagery.get_forest_cover(region)
316+
previous_cover = satellite_imagery.get_historical_cover(region, years_ago=1)
317+
318+
deforestation_rate = (previous_cover - current_cover) / previous_cover
319+
if deforestation_rate > 0.05: # 5% threshold
320+
alert_conservation_team(region, deforestation_rate)
321+
322+
return deforestation_rate
323+
\`\`\`
324+
254325
### Restoration Efforts
255326
256327
The UN Decade on Ecosystem Restoration aims to restore **350 million hectares** by 2030:

0 commit comments

Comments
 (0)