generated from rnentjes/kotlin-server-web-empty
Compare commits
2 Commits
a8fcabc571
...
bc7cd5fb97
| Author | SHA1 | Date | |
|---|---|---|---|
| bc7cd5fb97 | |||
| 63c24f6355 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ bin/
|
||||
### .kotlin ###
|
||||
.kotlin
|
||||
kotlin-js-store
|
||||
gradle.properties
|
||||
|
||||
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
template
|
||||
8
.idea/artifacts/markdown_parser_js_0_1_0_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/markdown_parser_js_0_1_0_SNAPSHOT.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="markdown-parser-js-0.1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="markdown-parser-js-0.1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="markdown-parser.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
8
.idea/artifacts/markdown_parser_js_1_0_0.xml
generated
Normal file
8
.idea/artifacts/markdown_parser_js_1_0_0.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="markdown-parser-js-1.0.0">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="markdown-parser-js-1.0.0.jar">
|
||||
<element id="module-output" name="markdown-parser.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
8
.idea/artifacts/markdown_parser_jvm_0_1_0_SNAPSHOT.xml
generated
Normal file
8
.idea/artifacts/markdown_parser_jvm_0_1_0_SNAPSHOT.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="markdown-parser-jvm-0.1.0-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="markdown-parser-jvm-0.1.0-SNAPSHOT.jar">
|
||||
<element id="module-output" name="markdown-parser.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
8
.idea/artifacts/markdown_parser_jvm_1_0_0.xml
generated
Normal file
8
.idea/artifacts/markdown_parser_jvm_1_0_0.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="markdown-parser-jvm-1.0.0">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="markdown-parser-jvm-1.0.0.jar">
|
||||
<element id="module-output" name="markdown-parser.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,7 +3,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<component name="accountSettings">
|
||||
|
||||
7
LICENSE.txt
Normal file
7
LICENSE.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2025 H.Nentjes
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
100
build.gradle.kts
100
build.gradle.kts
@@ -1,9 +1,14 @@
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.1.10"
|
||||
kotlin("multiplatform") version "2.2.21"
|
||||
signing
|
||||
id("org.jetbrains.dokka") version "2.0.0"
|
||||
id("com.vanniktech.maven.publish") version "0.31.0"
|
||||
}
|
||||
|
||||
group = "nl.astraeus"
|
||||
version = "0.1.0-SNAPSHOT"
|
||||
version = "1.0.1"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -16,45 +21,80 @@ repositories {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvmToolchain(11)
|
||||
jvm()
|
||||
js {
|
||||
binaries.executable()
|
||||
browser {
|
||||
distribution {
|
||||
outputDirectory.set(File("$projectDir/web/"))
|
||||
}
|
||||
}
|
||||
binaries.library()
|
||||
browser {}
|
||||
}
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
api("nl.astraeus:kotlin-simple-logging:1.1.1")
|
||||
api("nl.astraeus:kotlin-css-generator:1.0.10")
|
||||
}
|
||||
}
|
||||
val commonTest by getting
|
||||
val jvmMain by getting {
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation("io.undertow:undertow-core:2.3.14.Final")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||
|
||||
implementation("org.xerial:sqlite-jdbc:3.32.3.2")
|
||||
implementation("com.zaxxer:HikariCP:4.0.3")
|
||||
implementation("nl.astraeus:simple-jdbc-stats:1.6.1") {
|
||||
exclude(group = "org.slf4j", module = "slf4j-api")
|
||||
}
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
val jsMain by getting {
|
||||
dependencies {
|
||||
implementation("nl.astraeus:kotlin-komponent:1.2.5")
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
val jsTest by getting
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "gitea"
|
||||
setUrl("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
|
||||
|
||||
credentials {
|
||||
val giteaUsername: String? by project
|
||||
val giteaPassword: String? by project
|
||||
|
||||
username = giteaUsername
|
||||
password = giteaPassword
|
||||
}
|
||||
}
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<AbstractPublishToMaven> {
|
||||
dependsOn(tasks.withType<Sign>())
|
||||
}
|
||||
|
||||
signing {
|
||||
sign(publishing.publications)
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||
|
||||
signAllPublications()
|
||||
|
||||
coordinates(group.toString(), name, version.toString())
|
||||
|
||||
pom {
|
||||
name = "markdown-parser"
|
||||
description = "Markdown parser"
|
||||
inceptionYear = "2025"
|
||||
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
|
||||
licenses {
|
||||
license {
|
||||
name = "MIT"
|
||||
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "rnentjes"
|
||||
name = "Rien Nentjes"
|
||||
email = "info@nentjes.com"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
kotlin.code.style=official
|
||||
@@ -1,5 +1,21 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
val REPO_NAME = "dummy so the gitea template compiles, please remove"
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||
}
|
||||
|
||||
rootProject.name = "markdown-parser"
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
sealed class MarkdownPart {
|
||||
|
||||
data object NewLine : MarkdownPart()
|
||||
|
||||
data object PageBreak : MarkdownPart()
|
||||
|
||||
sealed class ParagraphPart() {
|
||||
data class Text(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
|
||||
data object LineBreak : ParagraphPart()
|
||||
|
||||
data class Link(
|
||||
val url: String,
|
||||
val label: String? = null,
|
||||
val title: String? = null,
|
||||
) : ParagraphPart()
|
||||
|
||||
data class Image(
|
||||
val alt: String,
|
||||
val src: String,
|
||||
val url: String? = null,
|
||||
) : ParagraphPart()
|
||||
|
||||
data class Bold(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
|
||||
data class Italic(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
|
||||
class BoldItalic(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
|
||||
class StrikeThrough(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
|
||||
class InlineCode(
|
||||
val text: String
|
||||
) : ParagraphPart()
|
||||
}
|
||||
|
||||
data class Paragraph(
|
||||
val parts: List<ParagraphPart>
|
||||
) : MarkdownPart()
|
||||
|
||||
data class Header(
|
||||
val text: String,
|
||||
val size: Int
|
||||
) : MarkdownPart()
|
||||
|
||||
data class UnorderedList(
|
||||
val lines: List<String>,
|
||||
) : MarkdownPart()
|
||||
|
||||
data class OrderedList(
|
||||
val lines: List<String>,
|
||||
) : MarkdownPart()
|
||||
|
||||
data class CodeBlock(
|
||||
val text: String,
|
||||
val language: String
|
||||
) : MarkdownPart()
|
||||
|
||||
data class Table(
|
||||
val headers: List<String>,
|
||||
val rows: List<List<String>>,
|
||||
) : MarkdownPart()
|
||||
|
||||
class Ruler() : MarkdownPart()
|
||||
}
|
||||
174
src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt
Normal file
174
src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt
Normal file
@@ -0,0 +1,174 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
import nl.astraeus.markdown.parser.MarkdownPart.ParagraphPart.*
|
||||
|
||||
private enum class ParType {
|
||||
TEXT,
|
||||
LINK_LABEL,
|
||||
LINK_URL,
|
||||
LINK_TITLE,
|
||||
LINK_END,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
BOLD_ITALIC,
|
||||
STRIKETHROUGH,
|
||||
INLINE_CODE,
|
||||
IMAGE_ALT,
|
||||
IMAGE_SRC,
|
||||
LINK_IMAGE_ALT,
|
||||
LINK_IMAGE_SRC,
|
||||
LINK_IMAGE_LINK,
|
||||
}
|
||||
|
||||
private typealias ParagraphData = MutableMap<ParType, String>
|
||||
|
||||
private data class ParState(
|
||||
val fromType: ParType,
|
||||
val text: String,
|
||||
val toType: ParType,
|
||||
val out: (ParagraphData) -> MarkdownPart.ParagraphPart? = { _ -> null }
|
||||
)
|
||||
|
||||
private val states = listOf(
|
||||
// Image with link
|
||||
ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.LINK_IMAGE_ALT, "](", ParType.LINK_IMAGE_SRC),
|
||||
ParState(ParType.LINK_IMAGE_SRC, ")](", ParType.LINK_IMAGE_LINK),
|
||||
ParState(ParType.LINK_IMAGE_LINK, ")", ParType.TEXT) { data ->
|
||||
Image(
|
||||
data[ParType.LINK_IMAGE_ALT]!!,
|
||||
data[ParType.LINK_IMAGE_SRC]!!,
|
||||
data[ParType.LINK_IMAGE_LINK],
|
||||
)
|
||||
},
|
||||
|
||||
// Image without link
|
||||
ParState(ParType.TEXT, "![", ParType.IMAGE_ALT) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.IMAGE_ALT, "](", ParType.IMAGE_SRC),
|
||||
ParState(ParType.IMAGE_SRC, ")", ParType.TEXT) { data ->
|
||||
Image(
|
||||
data[ParType.IMAGE_ALT]!!,
|
||||
data[ParType.IMAGE_SRC]!!,
|
||||
)
|
||||
},
|
||||
|
||||
// Links
|
||||
ParState(ParType.TEXT, "[", ParType.LINK_LABEL) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.LINK_LABEL, "](", ParType.LINK_URL),
|
||||
ParState(ParType.LINK_LABEL, "]", ParType.LINK_URL) { data ->
|
||||
Text(data[ParType.LINK_LABEL]!!)
|
||||
},
|
||||
ParState(ParType.LINK_URL, ")", ParType.TEXT) { data ->
|
||||
Link(data[ParType.LINK_URL]!!, data[ParType.LINK_LABEL])
|
||||
},
|
||||
|
||||
ParState(ParType.LINK_URL, "\"", ParType.LINK_TITLE),
|
||||
ParState(ParType.LINK_TITLE, "\"", ParType.LINK_END),
|
||||
ParState(ParType.LINK_END, ")", ParType.TEXT) { data ->
|
||||
Link(
|
||||
data[ParType.LINK_URL]!!,
|
||||
data[ParType.LINK_LABEL],
|
||||
data[ParType.LINK_TITLE],
|
||||
)
|
||||
},
|
||||
|
||||
ParState(ParType.TEXT, "***", ParType.BOLD_ITALIC) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.BOLD_ITALIC, "***", ParType.TEXT) { data ->
|
||||
BoldItalic(data[ParType.BOLD_ITALIC]!!)
|
||||
},
|
||||
|
||||
ParState(ParType.TEXT, "~~", ParType.STRIKETHROUGH) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.STRIKETHROUGH, "~~", ParType.TEXT) { data ->
|
||||
StrikeThrough(data[ParType.STRIKETHROUGH]!!)
|
||||
},
|
||||
|
||||
ParState(ParType.TEXT, "**", ParType.BOLD) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.BOLD, "**", ParType.TEXT) { data ->
|
||||
Bold(data[ParType.BOLD]!!)
|
||||
},
|
||||
|
||||
ParState(ParType.TEXT, "*", ParType.ITALIC) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.ITALIC, "*", ParType.TEXT) { data ->
|
||||
BoldItalic(data[ParType.ITALIC]!!)
|
||||
},
|
||||
|
||||
ParState(ParType.TEXT, "`", ParType.INLINE_CODE) { data ->
|
||||
Text(data[ParType.TEXT]!!)
|
||||
},
|
||||
ParState(ParType.INLINE_CODE, "`", ParType.TEXT) { data ->
|
||||
InlineCode(data[ParType.INLINE_CODE]!!)
|
||||
},
|
||||
)
|
||||
|
||||
private fun String.test(index: Int, value: String): Boolean {
|
||||
return this.length > index + value.length && this.substring(index, index + value.length) == value
|
||||
}
|
||||
|
||||
fun parseParagraph(text: String): MarkdownPart.Paragraph {
|
||||
val result = mutableListOf<MarkdownPart.ParagraphPart>()
|
||||
val buffer = StringBuilder()
|
||||
var type = ParType.TEXT
|
||||
val data: ParagraphData = mutableMapOf()
|
||||
var index = 0
|
||||
var activeStates = states.filter { it.fromType == type }
|
||||
|
||||
while (index < text.length) {
|
||||
var found = false
|
||||
for (state in activeStates) {
|
||||
if (state.fromType == type && text.test(index, state.text)) {
|
||||
data[state.fromType] = buffer.toString()
|
||||
buffer.clear()
|
||||
state.out(data)?.let {
|
||||
if (it !is Text || it.text.isNotBlank()) {
|
||||
result.add(it)
|
||||
}
|
||||
}
|
||||
type = state.toType
|
||||
index += state.text.length
|
||||
found = true
|
||||
activeStates = states.filter { it.fromType == type }
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
val ch = text[index]
|
||||
if (ch == '\n') {
|
||||
// Markdown hard line break: two or more spaces at end of line
|
||||
if (buffer.length >= 2 && buffer.endsWith(" ")) {
|
||||
val textBefore = buffer.substring(0, buffer.length - 2)
|
||||
if (textBefore.isNotEmpty()) {
|
||||
result.add(Text(textBefore))
|
||||
}
|
||||
result.add(LineBreak)
|
||||
buffer.clear()
|
||||
} else {
|
||||
// Keep original behavior for soft breaks (collapse later in HTML)
|
||||
buffer.append(ch)
|
||||
}
|
||||
} else {
|
||||
buffer.append(ch)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.isNotEmpty()) {
|
||||
result.add(Text(buffer.toString()))
|
||||
}
|
||||
|
||||
return MarkdownPart.Paragraph(result)
|
||||
}
|
||||
166
src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt
Normal file
166
src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt
Normal file
@@ -0,0 +1,166 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
enum class MarkdownType {
|
||||
CODE,
|
||||
PARAGRAPH,
|
||||
ORDERED_LIST,
|
||||
UNORDERED_LIST,
|
||||
TABLE,
|
||||
}
|
||||
|
||||
fun markdown(text: String): List<MarkdownPart> {
|
||||
val lines = text.lines()
|
||||
val parts = mutableListOf<MarkdownPart>()
|
||||
var language = ""
|
||||
var type = MarkdownType.PARAGRAPH
|
||||
var listIndex = 1
|
||||
|
||||
var index = 0
|
||||
val buffer = StringBuilder()
|
||||
|
||||
fun parseBuffer() {
|
||||
if (buffer.isNotBlank()) {
|
||||
parts.addAll(handleBuffer(type, buffer.toString(), language))
|
||||
}
|
||||
buffer.clear()
|
||||
type = MarkdownType.PARAGRAPH
|
||||
language = ""
|
||||
}
|
||||
|
||||
while (index < lines.size) {
|
||||
val rawLine = lines[index]
|
||||
val line = rawLine.trim()
|
||||
//println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}")
|
||||
when {
|
||||
type == MarkdownType.ORDERED_LIST -> {
|
||||
if (!line.startsWith("${listIndex++}.")) {
|
||||
parseBuffer()
|
||||
continue
|
||||
} else {
|
||||
buffer.append(line.substring(2))
|
||||
buffer.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
type == MarkdownType.UNORDERED_LIST -> {
|
||||
if (!line.startsWith("- ") &&
|
||||
!line.startsWith("* ")
|
||||
) {
|
||||
parseBuffer()
|
||||
continue
|
||||
} else {
|
||||
buffer.append(line.substring(2))
|
||||
buffer.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
type == MarkdownType.TABLE -> {
|
||||
if (!line.startsWith("|")) {
|
||||
parseBuffer()
|
||||
continue
|
||||
} else {
|
||||
buffer.append(line)
|
||||
buffer.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
type == MarkdownType.PARAGRAPH && line.isBlank() -> {
|
||||
buffer.append("\n")
|
||||
parseBuffer()
|
||||
}
|
||||
|
||||
line.startsWith("```") -> {
|
||||
if (type != MarkdownType.CODE) {
|
||||
parseBuffer()
|
||||
type = MarkdownType.CODE
|
||||
language = line.substring(3).trim()
|
||||
} else {
|
||||
parseBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
type == MarkdownType.CODE -> {
|
||||
buffer.append(rawLine)
|
||||
buffer.append("\n")
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
line.startsWith("1.") -> {
|
||||
parseBuffer()
|
||||
type = MarkdownType.ORDERED_LIST
|
||||
listIndex = 2
|
||||
buffer.append(line.substring(2))
|
||||
buffer.append("\n")
|
||||
}
|
||||
|
||||
line.startsWith("- ") || line.startsWith("* ") -> {
|
||||
parseBuffer()
|
||||
type = MarkdownType.UNORDERED_LIST
|
||||
buffer.append(line.substring(2))
|
||||
buffer.append("\n")
|
||||
}
|
||||
|
||||
line.startsWith("|") -> {
|
||||
parseBuffer()
|
||||
type = MarkdownType.TABLE
|
||||
buffer.append(line)
|
||||
buffer.append("\n")
|
||||
}
|
||||
|
||||
line.startsWith("---") -> {
|
||||
parseBuffer()
|
||||
parts.add(MarkdownPart.Ruler())
|
||||
}
|
||||
|
||||
line.startsWith("#") -> {
|
||||
parseBuffer()
|
||||
val headerLevel = line.takeWhile { it == '#' }.length
|
||||
val headerText = line.substring(headerLevel).trim()
|
||||
parts.add(MarkdownPart.Header(headerText, headerLevel))
|
||||
}
|
||||
|
||||
line == "[break]" -> {
|
||||
parseBuffer()
|
||||
parts.add(MarkdownPart.PageBreak)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Preserve trailing spaces for hard line breaks (two spaces at end of line)
|
||||
buffer.append(rawLine)
|
||||
buffer.append("\n")
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
parseBuffer()
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
private fun handleBuffer(
|
||||
type: MarkdownType,
|
||||
text: String,
|
||||
language: String = ""
|
||||
): List<MarkdownPart> = when (type) {
|
||||
MarkdownType.CODE -> {
|
||||
listOf(MarkdownPart.CodeBlock(text, language))
|
||||
}
|
||||
|
||||
MarkdownType.PARAGRAPH -> {
|
||||
listOf(parseParagraph(text))
|
||||
}
|
||||
|
||||
MarkdownType.ORDERED_LIST -> {
|
||||
listOf(MarkdownPart.OrderedList(text.lines()))
|
||||
}
|
||||
|
||||
MarkdownType.UNORDERED_LIST -> {
|
||||
listOf(MarkdownPart.UnorderedList(text.lines()))
|
||||
}
|
||||
|
||||
MarkdownType.TABLE -> {
|
||||
parseTable(text)
|
||||
}
|
||||
}
|
||||
48
src/commonMain/kotlin/nl/astraeus/markdown/parser/Table.kt
Normal file
48
src/commonMain/kotlin/nl/astraeus/markdown/parser/Table.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
fun parseTable(text: String): List<MarkdownPart> {
|
||||
val lines = text.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
|
||||
fun parseCells(line: String): List<String> {
|
||||
val trimmed = line.trim().trim('|')
|
||||
return if (trimmed.isEmpty()) emptyList() else trimmed.split("|").map { it.trim() }
|
||||
}
|
||||
|
||||
fun isSeparatorRow(cells: List<String>): Boolean {
|
||||
if (cells.isEmpty()) return false
|
||||
return cells.all { cell ->
|
||||
val dashCount = cell.count { it == '-' }
|
||||
val cleaned = cell.replace("-", "").replace("|", "").trim()
|
||||
dashCount >= 3 && cleaned.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return if (lines.size < 2) {
|
||||
// Not enough lines to be a table, fallback to code block
|
||||
listOf(MarkdownPart.CodeBlock(text, "table"))
|
||||
} else {
|
||||
val headerCells = parseCells(lines.first())
|
||||
val sepCells = parseCells(lines[1])
|
||||
|
||||
if (headerCells.isEmpty() || !isSeparatorRow(sepCells)) {
|
||||
// Invalid table format, fallback to code block
|
||||
listOf(MarkdownPart.CodeBlock(text, "table"))
|
||||
} else {
|
||||
val colCount = headerCells.size
|
||||
val rows = mutableListOf<List<String>>()
|
||||
for (i in 2 until lines.size) {
|
||||
val rowCells = parseCells(lines[i]).toMutableList()
|
||||
// Normalize column count to headers size
|
||||
if (rowCells.size < colCount) {
|
||||
while (rowCells.size < colCount) rowCells.add("")
|
||||
} else if (rowCells.size > colCount) {
|
||||
// Trim extras
|
||||
while (rowCells.size > colCount) rowCells.removeAt(rowCells.lastIndex)
|
||||
}
|
||||
rows.add(rowCells)
|
||||
}
|
||||
|
||||
listOf(MarkdownPart.Table(headers = headerCells, rows = rows))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
import kotlin.test.Test
|
||||
|
||||
class ParseTest {
|
||||
|
||||
@Test
|
||||
fun testParagraph() {
|
||||
val input = """
|
||||
Dit is een **test**, laat ***mij*** maar eens zien!
|
||||
|
||||
link: [NOS](www.nos.nl "Nos title") of zo.
|
||||
|
||||
|
||||
- link: [NU](www.nu.nl "Nu site") of zo.
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
val md = markdown(input)
|
||||
|
||||
printMarkdownParts(md)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testImage() {
|
||||
val input = """
|
||||
[](https://upload.wikimedia.org/wikipedia/commons.png)
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
val md = markdown(input)
|
||||
|
||||
printMarkdownParts(md)
|
||||
}
|
||||
|
||||
private fun printMarkdownParts(md: List<MarkdownPart>) {
|
||||
for (part in md) {
|
||||
if (part is MarkdownPart.Paragraph) {
|
||||
for (para in part.parts) {
|
||||
println("PARA: ${para::class.simpleName} - ${para.toString().take(75)}")
|
||||
}
|
||||
} else {
|
||||
println("PART: ${part::class.simpleName} - ${part.toString().take(75)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package nl.astraeus.markdown.parser
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TestParagraph {
|
||||
|
||||
@Test
|
||||
fun testBold() {
|
||||
val input = "Text **bold** Text"
|
||||
|
||||
val result = parseParagraph(input)
|
||||
|
||||
assertEquals(3, result.parts.size)
|
||||
|
||||
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
|
||||
assertTrue(result.parts[1] is MarkdownPart.ParagraphPart.Bold)
|
||||
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package nl.astraeus.tmpl
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import nl.astraeus.logger.Logger
|
||||
import nl.astraeus.tmpl.db.Database
|
||||
|
||||
val log = Logger()
|
||||
|
||||
val REPO_NAME = "dummy so the gitea template compiles, please remove"
|
||||
|
||||
fun main() {
|
||||
Thread.currentThread().uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
|
||||
log.warn(e) {
|
||||
e.message
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
object : Thread() {
|
||||
override fun run() {
|
||||
Database.vacuumDatabase()
|
||||
Database.closeDatabase()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Class.forName("nl.astraeus.jdbc.Driver")
|
||||
Database.initialize(HikariConfig().apply {
|
||||
driverClassName = "nl.astraeus.jdbc.Driver"
|
||||
jdbcUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/markdown-parser.db"
|
||||
username = "sa"
|
||||
password = ""
|
||||
maximumPoolSize = 25
|
||||
isAutoCommit = false
|
||||
|
||||
validate()
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package nl.astraeus.tmpl.db
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.set
|
||||
import kotlin.use
|
||||
|
||||
private val currentConnection = ThreadLocal<Connection>()
|
||||
|
||||
fun <T> transaction(
|
||||
block: (Connection) -> T
|
||||
): T {
|
||||
val hasConnection = currentConnection.get() != null
|
||||
var oldConnection: Connection? = null
|
||||
|
||||
if (!hasConnection) {
|
||||
currentConnection.set(Database.getConnection())
|
||||
}
|
||||
|
||||
val connection = currentConnection.get()
|
||||
|
||||
try {
|
||||
val result = block(connection)
|
||||
|
||||
connection.commit()
|
||||
|
||||
return result
|
||||
} finally {
|
||||
if (!hasConnection) {
|
||||
currentConnection.set(oldConnection)
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Database {
|
||||
|
||||
var ds: HikariDataSource? = null
|
||||
|
||||
fun initialize(config: HikariConfig) {
|
||||
val properties = Properties()
|
||||
properties["journal_mode"] = "WAL"
|
||||
|
||||
config.dataSourceProperties = properties
|
||||
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||
|
||||
ds = HikariDataSource(config)
|
||||
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
|
||||
Migrations.updateDatabaseIfNeeded()
|
||||
}
|
||||
|
||||
fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
|
||||
|
||||
fun vacuumDatabase() {
|
||||
getConnection().use {
|
||||
it.autoCommit = true
|
||||
|
||||
it.prepareStatement("VACUUM").use { ps ->
|
||||
ps.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeDatabase() {
|
||||
ds?.close()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package nl.astraeus.tmpl.db
|
||||
|
||||
import nl.astraeus.tmpl.log
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.sql.Timestamp
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
sealed class Migration {
|
||||
class Query(
|
||||
val query: String
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
class Code(
|
||||
val code: (Connection) -> Unit
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return code.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val DATABASE_MIGRATIONS = arrayOf<Migration>(
|
||||
Migration.Query(
|
||||
"""
|
||||
CREATE TABLE DATABASE_VERSION (
|
||||
ID INTEGER PRIMARY KEY,
|
||||
QUERY TEXT,
|
||||
EXECUTED TIMESTAMP
|
||||
)
|
||||
""".trimIndent()
|
||||
),
|
||||
Migration.Query("SELECT sqlite_version()"),
|
||||
)
|
||||
|
||||
object Migrations {
|
||||
var databaseVersionTableCreated = AtomicBoolean(false)
|
||||
|
||||
fun updateDatabaseIfNeeded() {
|
||||
try {
|
||||
transaction { con ->
|
||||
con.prepareStatement(
|
||||
"""
|
||||
SELECT MAX(ID) FROM DATABASE_VERSION
|
||||
""".trimIndent()
|
||||
).use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
databaseVersionTableCreated.compareAndSet(false, true)
|
||||
|
||||
if(rs.next()) {
|
||||
val maxId = rs.getInt(1)
|
||||
|
||||
for (index in maxId + 1..<DATABASE_MIGRATIONS.size) {
|
||||
executeMigration(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SQLException) {
|
||||
if (databaseVersionTableCreated.compareAndSet(false, true)) {
|
||||
executeMigration(0)
|
||||
updateDatabaseIfNeeded()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeMigration(index: Int) {
|
||||
transaction { con ->
|
||||
log.debug {
|
||||
"Executing migration index - [DATABASE_MIGRATIONS[index]]"
|
||||
}
|
||||
val description = when(
|
||||
val migration = DATABASE_MIGRATIONS[index]
|
||||
) {
|
||||
is Migration.Query -> {
|
||||
con.prepareStatement(migration.query).use { ps ->
|
||||
ps.execute()
|
||||
}
|
||||
|
||||
migration.query
|
||||
}
|
||||
is Migration.Code -> {
|
||||
migration.code(con)
|
||||
|
||||
migration.code.toString()
|
||||
}
|
||||
}
|
||||
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
|
||||
ps.setInt(1, index)
|
||||
ps.setString(2, description)
|
||||
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
|
||||
|
||||
ps.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user