Skip to content

Commit e4e81aa

Browse files
committed
- add rememberMarkdownState accepting a suspending block to load the markdown data (e.g. from resource files, ...
- add progress indicator to sample
1 parent f450216 commit e4e81aa

File tree

2 files changed

+131
-27
lines changed
  • multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model
  • sample/shared/src/commonMain/kotlin/com/mikepenz/markdown/sample

2 files changed

+131
-27
lines changed

multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownState.kt

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import org.intellij.markdown.parser.MarkdownParser
2626
* @param flavour The [MarkdownFlavourDescriptor] to use for parsing.
2727
* @param parser The [MarkdownParser] to use for parsing.
2828
* @param referenceLinkHandler The [ReferenceLinkHandler] to use for storing links.
29-
* @param immediate Whether to parse the content immediately or not. (WARNING: This is not advices, as it will block the composition!)
29+
* @param immediate Whether to parse the content immediately or not. (WARNING: This is not advised, as it will block the composition!)
30+
* @return A [MarkdownState] instance that will parse the content and emit the result to its [MarkdownState.state] flow.
3031
*/
3132
@Composable
3233
fun rememberMarkdownState(
@@ -37,42 +38,141 @@ fun rememberMarkdownState(
3738
referenceLinkHandler: ReferenceLinkHandler = ReferenceLinkHandlerImpl(),
3839
immediate: Boolean = LocalInspectionMode.current,
3940
): MarkdownState {
40-
val input = Input(
41-
content = content,
42-
lookupLinks = lookupLinks,
43-
flavour = flavour,
44-
parser = parser,
45-
referenceLinkHandler = referenceLinkHandler,
46-
)
47-
val state = remember(input) { MarkdownState(input) }
41+
val input = remember(content, lookupLinks, flavour, parser, referenceLinkHandler) {
42+
Input(
43+
content = content,
44+
lookupLinks = lookupLinks,
45+
flavour = flavour,
46+
parser = parser,
47+
referenceLinkHandler = referenceLinkHandler,
48+
)
49+
}
50+
val state = remember(input) { MarkdownStateImpl(input) }
51+
4852
if (immediate) {
53+
// In immediate mode, parse synchronously but be aware this blocks the UI thread
4954
state.parseBlocking()
5055
} else {
56+
// Otherwise, parse asynchronously in a coroutine
5157
LaunchedEffect(state) {
5258
state.parse()
5359
}
5460
}
61+
5562
return state
5663
}
5764

5865
/**
59-
* A [MarkdownState] that that executes the parsing of the markdown content with the [MarkdownParser] asynchronously.
66+
* A [MarkdownState] that executes the parsing of the markdown content with the [MarkdownParser] asynchronously.
67+
* This version accepts a suspend function block that returns the markdown content, allowing for dynamic loading.
68+
*
69+
* @param block A suspend function that returns the markdown content to parse.
70+
* @param lookupLinks Whether to lookup links in the parsed tree or not.
71+
* @param flavour The [MarkdownFlavourDescriptor] to use for parsing.
72+
* @param parser The [MarkdownParser] to use for parsing.
73+
* @param referenceLinkHandler The [ReferenceLinkHandler] to use for storing links.
74+
* @return A [MarkdownState] instance that will parse the content and emit the result to its [MarkdownState.state] flow.
75+
*/
76+
@Composable
77+
fun rememberMarkdownState(
78+
lookupLinks: Boolean = true,
79+
flavour: MarkdownFlavourDescriptor = GFMFlavourDescriptor(),
80+
parser: MarkdownParser = MarkdownParser(flavour),
81+
referenceLinkHandler: ReferenceLinkHandler = ReferenceLinkHandlerImpl(),
82+
block: suspend () -> String,
83+
): MarkdownState {
84+
// Create an initial state with empty content
85+
val initialInput = remember(lookupLinks, flavour, parser, referenceLinkHandler) {
86+
Input(
87+
content = "",
88+
lookupLinks = lookupLinks,
89+
flavour = flavour,
90+
parser = parser,
91+
referenceLinkHandler = referenceLinkHandler,
92+
)
93+
}
94+
val state = remember(block, initialInput) {
95+
MarkdownStateImpl(initialInput)
96+
}
97+
98+
// Launch a coroutine to fetch the content and update the state
99+
LaunchedEffect(state) {
100+
try {
101+
val content = block()
102+
val input = Input(
103+
content = content,
104+
lookupLinks = lookupLinks,
105+
flavour = flavour,
106+
parser = parser,
107+
referenceLinkHandler = referenceLinkHandler,
108+
)
109+
state.updateInput(input)
110+
state.parse()
111+
} catch (e: Throwable) {
112+
state.setError(e)
113+
}
114+
}
115+
116+
return state
117+
}
118+
119+
/**
120+
* Interface for a state that handles the parsing of markdown content with the [MarkdownParser].
121+
*/
122+
@Stable
123+
interface MarkdownState {
124+
/** The current state of the markdown parsing */
125+
val state: StateFlow<State>
126+
127+
/** The links found in the markdown content */
128+
val links: StateFlow<Map<String, String?>>
129+
130+
/**
131+
* Parses the markdown content asynchronously using the Default dispatcher.
132+
* When a result is available it will be emitted to the [state] flow.
133+
*/
134+
suspend fun parse(): State
135+
}
136+
137+
/**
138+
* Implementation of [MarkdownState] that executes the parsing of the markdown content with the [MarkdownParser] asynchronously.
60139
*/
61140
@Stable
62-
class MarkdownState(
63-
val input: Input,
64-
) {
141+
internal class MarkdownStateImpl(
142+
private var input: Input,
143+
) : MarkdownState {
65144
private val stateFlow: MutableStateFlow<State> = MutableStateFlow(State.Loading(input.referenceLinkHandler))
66-
val state: StateFlow<State> = stateFlow.asStateFlow()
145+
override val state: StateFlow<State> = stateFlow.asStateFlow()
67146

68147
private val linkStateFlow: MutableStateFlow<Map<String, String?>> = MutableStateFlow(emptyMap())
69-
val links: StateFlow<Map<String, String?>> = linkStateFlow.asStateFlow()
148+
override val links: StateFlow<Map<String, String?>> = linkStateFlow.asStateFlow()
149+
150+
/**
151+
* Updates the input for this markdown state.
152+
* This is used when loading content dynamically.
153+
*
154+
* @param newInput The new input to use for parsing.
155+
*/
156+
internal fun updateInput(newInput: Input) {
157+
input = newInput
158+
stateFlow.value = State.Loading(input.referenceLinkHandler)
159+
}
160+
161+
/**
162+
* Sets an error state for this markdown state.
163+
* This is used when an error occurs during dynamic content loading.
164+
*
165+
* @param error The error that occurred.
166+
*/
167+
internal fun setError(error: Throwable) {
168+
stateFlow.value = State.Error(error, input.referenceLinkHandler)
169+
}
70170

71171
/**
72172
* Parses the markdown content asynchronously using the Default dispatcher.
73173
* When a result is available it will be emitted to the [state] flow.
74174
*/
75-
suspend fun parse(): State = withContext(Dispatchers.Default) {
175+
override suspend fun parse(): State = withContext(Dispatchers.Default) {
76176
parseBlocking()
77177
}
78178

@@ -117,7 +217,7 @@ fun parseMarkdownFlow(
117217
referenceLinkHandler: ReferenceLinkHandler = ReferenceLinkHandlerImpl(),
118218
) = flow {
119219
emit(State.Loading(referenceLinkHandler))
120-
val markdownState = MarkdownState(
220+
val markdownState = MarkdownStateImpl(
121221
Input(
122222
content = content,
123223
lookupLinks = lookupLinks,

sample/shared/src/commonMain/kotlin/com/mikepenz/markdown/sample/MarkDownPage.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package com.mikepenz.markdown.sample
22

33
import androidx.compose.foundation.isSystemInDarkTheme
4+
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.fillMaxSize
56
import androidx.compose.foundation.layout.padding
67
import androidx.compose.foundation.rememberScrollState
78
import androidx.compose.foundation.text.selection.SelectionContainer
89
import androidx.compose.foundation.verticalScroll
10+
import androidx.compose.material3.CircularProgressIndicator
911
import androidx.compose.runtime.Composable
10-
import androidx.compose.runtime.LaunchedEffect
11-
import androidx.compose.runtime.getValue
12-
import androidx.compose.runtime.mutableStateOf
1312
import androidx.compose.runtime.remember
14-
import androidx.compose.runtime.saveable.rememberSaveable
15-
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.Alignment
1614
import androidx.compose.ui.Modifier
1715
import androidx.compose.ui.unit.dp
1816
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
@@ -39,14 +37,12 @@ internal fun MarkDownPage(modifier: Modifier = Modifier) {
3937
val highlightsBuilder = remember(isDarkTheme) {
4038
Highlights.Builder().theme(SyntaxThemes.atom(darkMode = isDarkTheme))
4139
}
42-
var markdown by rememberSaveable(Unit) { mutableStateOf("") }
43-
LaunchedEffect(Unit) {
44-
markdown = Res.readBytes("files/sample.md").decodeToString()
45-
}
4640

4741
SelectionContainer {
4842
Markdown(
49-
markdownState = rememberMarkdownState(markdown),
43+
markdownState = rememberMarkdownState {
44+
Res.readBytes("files/sample.md").decodeToString()
45+
},
5046
components = markdownComponents(
5147
codeBlock = {
5248
MarkdownHighlightedCodeBlock(
@@ -74,6 +70,14 @@ internal fun MarkDownPage(modifier: Modifier = Modifier) {
7470
)
7571
}
7672
},
73+
loading = {
74+
Box(
75+
modifier = modifier.fillMaxSize(),
76+
contentAlignment = Alignment.Center
77+
) {
78+
CircularProgressIndicator()
79+
}
80+
},
7781
modifier = modifier
7882
.fillMaxSize()
7983
.verticalScroll(rememberScrollState())

0 commit comments

Comments
 (0)