Skip to content

Commit b43d418

Browse files
leandroBorgesFerreiraLeandro Ferreira
andauthored
Simple import for markdown (#438)
* Simple import for markdown * Update FileChooserLoad.jvm.kt * Update FileChooserLoad.jvm.kt --------- Co-authored-by: Leandro Ferreira <[email protected]>
1 parent b8388c5 commit b43d418

File tree

9 files changed

+313
-38
lines changed

9 files changed

+313
-38
lines changed

application/features/note_menu/src/commonMain/kotlin/io/writeopia/notemenu/viewmodel/ChooseNoteKmpViewModel.kt

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,27 @@ import androidx.lifecycle.viewModelScope
55
import io.writeopia.auth.core.data.User
66
import io.writeopia.auth.core.manager.AuthManager
77
import io.writeopia.common.utils.DISCONNECTED_USER_ID
8+
import io.writeopia.common.utils.NotesNavigation
89
import io.writeopia.common.utils.ResultData
910
import io.writeopia.common.utils.file.FileUtils
1011
import io.writeopia.common.utils.file.SaveImage
1112
import io.writeopia.common.utils.map
1213
import io.writeopia.common.utils.toBoolean
1314
import io.writeopia.commonui.extensions.toUiCard
14-
import io.writeopia.core.configuration.repository.ConfigurationRepository
15-
import io.writeopia.sdk.models.document.Folder
1615
import io.writeopia.core.configuration.models.NotesArrangement
17-
import io.writeopia.models.interfaces.configuration.WorkspaceConfigRepository
18-
import io.writeopia.common.utils.NotesNavigation
16+
import io.writeopia.core.configuration.repository.ConfigurationRepository
1917
import io.writeopia.core.folders.repository.NotesUseCase
18+
import io.writeopia.models.interfaces.configuration.WorkspaceConfigRepository
2019
import io.writeopia.notemenu.ui.dto.NotesUi
2120
import io.writeopia.onboarding.OnboardingState
2221
import io.writeopia.sdk.export.DocumentToJson
2322
import io.writeopia.sdk.export.DocumentToMarkdown
2423
import io.writeopia.sdk.export.DocumentToTxt
2524
import io.writeopia.sdk.export.DocumentWriter
2625
import io.writeopia.sdk.import.json.WriteopiaJsonParser
26+
import io.writeopia.sdk.import.markdown.MarkdownToDocument
2727
import io.writeopia.sdk.models.document.Document
28+
import io.writeopia.sdk.models.document.Folder
2829
import io.writeopia.sdk.models.document.MenuItem
2930
import io.writeopia.sdk.models.files.ExternalFile
3031
import io.writeopia.sdk.models.id.GenerateId
@@ -379,6 +380,7 @@ internal class ChooseNoteKmpViewModel(
379380

380381
viewModelScope.launch(Dispatchers.Default) {
381382
importJsonNotes(filePaths, now)
383+
importMarkdownNotes(filePaths, now)
382384
importImages(filePaths, now)
383385
}
384386
}
@@ -484,6 +486,33 @@ internal class ChooseNoteKmpViewModel(
484486
.collect(notesUseCase::saveDocument)
485487
}
486488

489+
private suspend fun importMarkdownNotes(externalFiles: List<ExternalFile>, now: Instant) {
490+
externalFiles.filter { file -> file.extension == "md" }
491+
.map { file -> file.fullPath }
492+
.let { files ->
493+
MarkdownToDocument.readDocuments(files, getUserId(), notesNavigation.id)
494+
}
495+
.onCompletion { exception ->
496+
if (exception == null) {
497+
// refreshNotes()
498+
cancelEditMenu()
499+
}
500+
}
501+
.map { document ->
502+
println("content size: ${document.content.size}")
503+
504+
document.copy(
505+
parentId = notesNavigation.id,
506+
id = GenerateId.generate(),
507+
lastUpdatedAt = now,
508+
createdAt = now,
509+
userId = getUserId(),
510+
favorite = false
511+
)
512+
}
513+
.collect(notesUseCase::saveDocument)
514+
}
515+
487516
private suspend fun importImages(externalFiles: List<ExternalFile>, now: Instant) {
488517
externalFiles.filter { file -> supportedImageFiles.contains(file.extension) }
489518
.map { externalImage ->

plugins/writeopia_import_document/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ kotlin {
8181
implementation(libs.kotlinx.coroutines.core)
8282
}
8383
}
84+
85+
commonTest.dependencies {
86+
implementation(libs.kotlin.test)
87+
}
8488
}
8589
}
8690

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package io.writeopia.sdk.import.markdown
2+
3+
import io.writeopia.sdk.models.story.StoryTypes
4+
import io.writeopia.sdk.models.story.Tag
5+
import io.writeopia.sdk.serialization.data.StoryStepApi
6+
import io.writeopia.sdk.serialization.data.StoryTypeApi
7+
import io.writeopia.sdk.serialization.data.TagInfoApi
8+
9+
object MarkdownParser {
10+
11+
fun parse(lines: List<String>): List<StoryStepApi> {
12+
var acc = -1
13+
return lines.map { it.trim() }
14+
.filter { it.isNotEmpty() }
15+
.mapIndexed { i, trimmed ->
16+
acc++
17+
when {
18+
i == 0 && trimmed.startsWith("#") -> {
19+
val type = StoryTypes.TITLE.type
20+
StoryStepApi(
21+
type = StoryTypeApi(type.name, type.number),
22+
text = trimmed.drop(1).trimStart(),
23+
position = acc
24+
)
25+
}
26+
27+
i == 0 && !trimmed.startsWith("#") -> {
28+
val type = StoryTypes.TITLE.type
29+
StoryStepApi(
30+
type = StoryTypeApi(type.name, type.number),
31+
text = "",
32+
position = acc
33+
)
34+
}
35+
36+
trimmed.startsWith("####") -> {
37+
val type = StoryTypes.TEXT.type
38+
StoryStepApi(
39+
type = StoryTypeApi(type.name, type.number),
40+
text = trimmed.drop(4).trimStart(),
41+
tags = setOf(TagInfoApi(Tag.H4.name, 0)),
42+
position = acc
43+
)
44+
}
45+
46+
trimmed.startsWith("###") -> {
47+
val type = StoryTypes.TEXT.type
48+
StoryStepApi(
49+
type = StoryTypeApi(type.name, type.number),
50+
text = trimmed.drop(3).trimStart(),
51+
tags = setOf(TagInfoApi(Tag.H3.name, 0)),
52+
position = acc
53+
)
54+
}
55+
56+
trimmed.startsWith("##") -> {
57+
val type = StoryTypes.TEXT.type
58+
StoryStepApi(
59+
type = StoryTypeApi(type.name, type.number),
60+
text = trimmed.drop(2).trimStart(),
61+
tags = setOf(TagInfoApi(Tag.H2.name, 0)),
62+
position = acc
63+
)
64+
}
65+
66+
trimmed.startsWith("#") -> {
67+
val type = StoryTypes.TEXT.type
68+
StoryStepApi(
69+
type = StoryTypeApi(type.name, type.number),
70+
text = trimmed.drop(1).trimStart(),
71+
tags = setOf(TagInfoApi(Tag.H1.name, 0)),
72+
position = acc
73+
)
74+
}
75+
76+
// trimmed.matches(Regex("^\\d+\\.\\s+.*")) -> {
77+
// StoryType.OrderedListItem to trimmed.replace(Regex("^\\d+\\.\\s+"), "")
78+
// }
79+
80+
trimmed.startsWith("---") -> {
81+
val type = StoryTypes.DIVIDER.type
82+
StoryStepApi(
83+
type = StoryTypeApi(type.name, type.number),
84+
position = acc
85+
)
86+
}
87+
88+
trimmed.startsWith("- ") || trimmed.startsWith("* ") -> {
89+
val type = StoryTypes.TEXT.type
90+
StoryStepApi(
91+
type = StoryTypeApi(type.name, type.number),
92+
text = trimmed.drop(2).trimStart(),
93+
position = acc
94+
)
95+
}
96+
97+
else -> {
98+
val type = StoryTypes.TEXT.type
99+
StoryStepApi(
100+
type = StoryTypeApi(type.name, type.number),
101+
text = trimmed,
102+
position = acc
103+
)
104+
}
105+
}
106+
}
107+
}
108+
}
Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
11
package io.writeopia.sdk.import.markdown
22

3-
public class MarkdownToDocument(
4-
private val charactersReverseIndexParser: CharactersReverseIndexParser
5-
) {
3+
import io.writeopia.sdk.models.document.Document
4+
import kotlinx.coroutines.flow.Flow
65

7-
// fun parse(inputReader: InputStream) {
8-
// inputReader.reader().forEachLine { line ->
9-
// charactersReverseIndexParser.
10-
// }
11-
// }
6+
expect object MarkdownToDocument {
127

13-
// private fun typeOfLine(
14-
// line: String,
15-
// default: StoryType = StoryTypes.MESSAGE.type,
16-
// allowedCommands: Set<Command> = CommandFactory.defaultCommands()
17-
// ): StoryType {
18-
// val reversedIndex = charactersReverseIndexParser.reversedIndex
19-
// val commandIdsPerPosition = mutableListOf<List<Int>>()
20-
//
21-
// val commands = allowedCommands.toMutableSet()
22-
//
23-
// // Stop when a space is reached. The command should keep looking until a space is should. Otherwise
24-
// // it should keep looking/ For example ###### is not a command, but it would match it not
25-
// // looking until a space is found.
26-
//
27-
// line.forEachIndexed { index, char ->
28-
// val indexes = reversedIndex[char]
29-
//
30-
// if (indexes == null) {
31-
// return default
32-
// } else {
33-
// commandIdsPerPosition.add(indexes.map { it.commandId })
34-
// }
35-
// }
36-
// }
8+
fun readDocuments(files: List<String>, userId: String, parentId: String): Flow<Document>
379
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.writeopia.sdk.imports.test
2+
3+
import io.writeopia.sdk.import.markdown.MarkdownParser
4+
import io.writeopia.sdk.models.story.StoryTypes
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
class MarkdownParserTest {
9+
10+
private val markdownSample = listOf(
11+
"# Sample Markdown Document",
12+
"",
13+
"Welcome to this **Markdown** example! This document showcases basic formatting elements.",
14+
"",
15+
"---",
16+
"",
17+
"## Table of Contents",
18+
"",
19+
"1. [Headings](#headings)",
20+
"2. [Lists](#lists)",
21+
"3. [Code](#code)",
22+
"4. [Blockquote](#blockquote)",
23+
"5. [Links and Images](#links-and-images)",
24+
"",
25+
"---",
26+
"",
27+
"## Headings",
28+
"",
29+
"Use `#` for H1, `##` for H2, and so on:",
30+
"",
31+
"### This is a Heading 3",
32+
"",
33+
"---",
34+
"",
35+
"## Lists",
36+
"",
37+
"### Unordered List",
38+
"",
39+
"- Apples",
40+
"- Bananas",
41+
"- Cherries",
42+
"",
43+
"### Ordered List",
44+
"",
45+
"1. First item",
46+
"2. Second item",
47+
"3. Third item",
48+
"",
49+
"---",
50+
"",
51+
"## Code",
52+
"",
53+
"Inline code looks like `this`.",
54+
"",
55+
"#### Code Block",
56+
"",
57+
"```python",
58+
"def hello_world():",
59+
" print(\"Hello, world!\")",
60+
"```",
61+
"",
62+
"---",
63+
"",
64+
"## Blockquote",
65+
"",
66+
"> “Markdown is not a replacement for HTML, but it's a syntax for writing for the web.” ",
67+
"> — *John Gruber*",
68+
"",
69+
"---",
70+
"",
71+
"## Links and Images",
72+
"",
73+
"- [Visit OpenAI](https://www.openai.com)",
74+
"- ![Sample Image](https://via.placeholder.com/150)",
75+
"",
76+
"---",
77+
"",
78+
"## Task List",
79+
"",
80+
"- [x] Learn Markdown",
81+
"- [ ] Write documentation",
82+
"- [ ] Share with team",
83+
"",
84+
"---",
85+
"",
86+
"Thanks for reading!"
87+
)
88+
89+
@Test
90+
fun `it should parse the document`() {
91+
val results = MarkdownParser.parse(markdownSample)
92+
93+
assertEquals(results.first().type.number, StoryTypes.TITLE.type.number)
94+
}
95+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.writeopia.sdk.import.markdown
2+
3+
import io.writeopia.sdk.models.document.Document
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.flow
6+
7+
actual object MarkdownToDocument {
8+
actual fun readDocuments(
9+
files: List<String>,
10+
userId: String,
11+
parentId: String
12+
): Flow<Document> = flow { }
13+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.writeopia.sdk.import.markdown
2+
3+
import io.writeopia.sdk.models.document.Document
4+
import io.writeopia.sdk.models.id.GenerateId
5+
import io.writeopia.sdk.models.story.StoryTypes
6+
import io.writeopia.sdk.serialization.data.DocumentApi
7+
import io.writeopia.sdk.serialization.extensions.toModel
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.asFlow
10+
import kotlinx.coroutines.flow.filter
11+
import kotlinx.coroutines.flow.map
12+
import java.io.File
13+
14+
actual object MarkdownToDocument {
15+
16+
actual fun readDocuments(
17+
files: List<String>,
18+
userId: String,
19+
parentId: String
20+
): Flow<Document> =
21+
files.asFlow()
22+
.map(::File)
23+
.filter { file -> file.extension == "md" }
24+
.map { file ->
25+
val content = MarkdownParser.parse(file.readLines())
26+
val title = content.firstOrNull { storyStepApi ->
27+
storyStepApi.type.number == StoryTypes.TITLE.type.number
28+
}?.text
29+
30+
DocumentApi(
31+
id = GenerateId.generate(),
32+
title = title ?: "",
33+
userId = userId,
34+
parentId = parentId,
35+
isLocked = false,
36+
content = content
37+
)
38+
}
39+
.map { it.toModel() }
40+
}

0 commit comments

Comments
 (0)