Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
class OllamaResponse(
val model: String? = null,
val created_at: Long? = null,
// val created_at: Long? = null,
val response: String? = null,
val done: Boolean? = null,
val done_reason: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,117 +1,125 @@
package io.writeopia.sdk.import.markdown

import io.writeopia.sdk.manager.InTextMarkdownHandler
import io.writeopia.sdk.models.story.StoryTypes
import io.writeopia.sdk.models.story.Tag
import io.writeopia.sdk.serialization.data.StoryStepApi
import io.writeopia.sdk.serialization.data.StoryTypeApi
import io.writeopia.sdk.serialization.data.TagInfoApi
import io.writeopia.sdk.serialization.extensions.toApi
import io.writeopia.sdk.serialization.extensions.toModel

object MarkdownParser {

fun parse(lines: List<String>): List<StoryStepApi> {
var acc = -1
// Take care when moving the code, order matters for comparations!
return lines.map { it.trim() }
.mapIndexed { i, trimmed ->
acc++
when {
i == 0 && trimmed.startsWith("#") -> {
val type = StoryTypes.TITLE.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(1).trimStart(),
position = acc
)
}
val stepApi = parseLine(acc, i, trimmed)

i == 0 && !trimmed.startsWith("#") -> {
val type = StoryTypes.TITLE.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = "",
position = acc
)
}
InTextMarkdownHandler.handleMarkdown(stepApi.toModel()).toApi(acc)
}
}

private fun parseLine(acc: Int, i: Int, trimmed: String) =
// Take care when moving the code, order matters!
when {
i == 0 && trimmed.startsWith("#") -> {
val type = StoryTypes.TITLE.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(1).trimStart(),
position = acc
)
}

trimmed.startsWith("####") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(4).trimStart(),
tags = setOf(TagInfoApi(Tag.H4.name, 0)),
position = acc
)
}
i == 0 && !trimmed.startsWith("#") -> {
val type = StoryTypes.TITLE.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = "",
position = acc
)
}

trimmed.startsWith("####") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(4).trimStart(),
tags = setOf(TagInfoApi(Tag.H4.name, 0)),
position = acc
)
}

trimmed.startsWith("###") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(3).trimStart(),
tags = setOf(TagInfoApi(Tag.H3.name, 0)),
position = acc
)
}
trimmed.startsWith("###") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(3).trimStart(),
tags = setOf(TagInfoApi(Tag.H3.name, 0)),
position = acc
)
}

trimmed.startsWith("##") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(2).trimStart(),
tags = setOf(TagInfoApi(Tag.H2.name, 0)),
position = acc
)
}
trimmed.startsWith("##") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(2).trimStart(),
tags = setOf(TagInfoApi(Tag.H2.name, 0)),
position = acc
)
}

trimmed.startsWith("#") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(1).trimStart(),
tags = setOf(TagInfoApi(Tag.H1.name, 0)),
position = acc
)
}
trimmed.startsWith("#") -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(1).trimStart(),
tags = setOf(TagInfoApi(Tag.H1.name, 0)),
position = acc
)
}

// trimmed.matches(Regex("^\\d+\\.\\s+.*")) -> {
// StoryType.OrderedListItem to trimmed.replace(Regex("^\\d+\\.\\s+"), "")
// }

trimmed.startsWith("---") -> {
val type = StoryTypes.DIVIDER.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
position = acc
)
}
trimmed.startsWith("---") -> {
val type = StoryTypes.DIVIDER.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
position = acc
)
}

trimmed.startsWith("[] ") || trimmed.startsWith("-[] ") -> {
val type = StoryTypes.CHECK_ITEM.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(3).trimStart(),
position = acc
)
}
trimmed.startsWith("[] ") || trimmed.startsWith("-[] ") -> {
val type = StoryTypes.CHECK_ITEM.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(3).trimStart(),
position = acc
)
}

trimmed.startsWith("- ") || trimmed.startsWith("* ") -> {
val type = StoryTypes.UNORDERED_LIST_ITEM.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(2).trimStart(),
position = acc
)
}
trimmed.startsWith("- ") || trimmed.startsWith("* ") -> {
val type = StoryTypes.UNORDERED_LIST_ITEM.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed.drop(2).trimStart(),
position = acc
)
}

else -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed,
position = acc
)
}
}
else -> {
val type = StoryTypes.TEXT.type
StoryStepApi(
type = StoryTypeApi(type.name, type.number),
text = trimmed,
position = acc
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class MarkdownParserTest {
val expected = listOf(
StoryTypes.TITLE.type.number to "Sample Markdown Document",
StoryTypes.TEXT.type.number to "",
StoryTypes.TEXT.type.number to "Welcome to this **Markdown** example!",
StoryTypes.TEXT.type.number to "Welcome to this Markdown example!",
)

val results = MarkdownParser.parse(sample)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.writeopia.sdk.manager

import io.writeopia.sdk.models.id.GenerateId
import io.writeopia.sdk.models.span.Span
import io.writeopia.sdk.models.span.SpanInfo
import io.writeopia.sdk.models.story.StoryStep

object InTextMarkdownHandler {

// Bold: Matches exactly **text**
private val BOLD_REGEX = Regex("""\*\*(?!\*)(.*?)\*\*""")

// Italic: Matches *text* but ensures the boundaries are not double asterisks
// (?<!\*) means "not preceded by *"
// (?!\*) means "not followed by *"
private val ITALIC_REGEX = Regex("""(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)""")

fun handleMarkdown(storyStep: StoryStep): StoryStep {
val originalText = storyStep.text ?: return storyStep

val newSpans = mutableSetOf<SpanInfo>()
var processedText = originalText

// Order matters: Process bold first to "clean" those markers
// before the italic logic runs.
processedText = processPattern(processedText, BOLD_REGEX, Span.BOLD, newSpans)
processedText = processPattern(processedText, ITALIC_REGEX, Span.ITALIC, newSpans)

return if (newSpans.isNotEmpty()) {
storyStep.copy(
text = processedText,
spans = storyStep.spans + newSpans,
localId = if (newSpans.isNotEmpty()) GenerateId.generate() else storyStep.localId
)
} else {
storyStep
}
}

private fun processPattern(
text: String,
regex: Regex,
spanType: Span,
spanSet: MutableSet<SpanInfo>
): String {
var currentText = text
var match = regex.find(currentText)

while (match != null) {
val fullMatchRange = match.range
val content = match.groupValues[1]

// Check to ensure there is actually content inside the tags
if (content.isNotEmpty()) {
val spanInfo = SpanInfo.create(
start = fullMatchRange.first,
end = fullMatchRange.first + content.length,
span = spanType
)
spanSet.add(spanInfo)
currentText = currentText.replaceRange(fullMatchRange, content)
} else {
// If it's just **, we move past it to avoid infinite loops
match = regex.find(currentText, fullMatchRange.first + 1)
continue
}

match = regex.find(currentText)
}
return currentText
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package io.writeopia.ui.manager

import io.writeopia.sdk.manager.DocumentTracker
import io.writeopia.sdk.manager.InTextMarkdownHandler
import io.writeopia.sdk.manager.WriteopiaManager
import io.writeopia.sdk.manager.fixMove
import io.writeopia.sdk.model.action.Action
Expand Down Expand Up @@ -90,6 +91,7 @@ class WriteopiaStateManager(
StoryTypes.CHECK_ITEM.type.number,
StoryTypes.UNORDERED_LIST_ITEM.type.number,
),
private val inTextMarkdownHandler: InTextMarkdownHandler? = InTextMarkdownHandler
) : BackstackHandler, BackstackInform by backStackManager {

private val selectionBuffer: EventBuffer<Pair<Boolean, Int>> = EventBuffer(coroutineScope)
Expand Down Expand Up @@ -572,7 +574,15 @@ class WriteopiaStateManager(
if (!isEditable) return
backStackManager.addTextState(_currentStory.value, stateChange.position)

changeStoryStateAndTrackIt(stateChange, trackIt = false)
val step = stateChange.storyStep

val newState = if (inTextMarkdownHandler != null) {
stateChange.copy(storyStep = inTextMarkdownHandler.handleMarkdown(step))
} else {
stateChange
}

changeStoryStateAndTrackIt(newState, trackIt = false)
}

/**
Expand Down