generated from rnentjes/kotlin-server-web-empty
Refactor project to focus on Markdown parser development. Removed unused database and template-related code, added Markdown parsing functionality, and updated build configuration.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ bin/
|
|||||||
### .kotlin ###
|
### .kotlin ###
|
||||||
.kotlin
|
.kotlin
|
||||||
kotlin-js-store
|
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">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
<component name="accountSettings">
|
<component name="accountSettings">
|
||||||
|
|||||||
100
build.gradle.kts
100
build.gradle.kts
@@ -1,9 +1,14 @@
|
|||||||
|
import com.vanniktech.maven.publish.SonatypeHost
|
||||||
|
|
||||||
plugins {
|
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"
|
group = "nl.astraeus"
|
||||||
version = "0.1.0-SNAPSHOT"
|
version = "1.0.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -16,45 +21,80 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(11)
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js {
|
||||||
binaries.executable()
|
binaries.library()
|
||||||
browser {
|
browser {}
|
||||||
distribution {
|
|
||||||
outputDirectory.set(File("$projectDir/web/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("nl.astraeus:kotlin-simple-logging:1.1.1")
|
|
||||||
api("nl.astraeus:kotlin-css-generator:1.0.10")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting
|
val commonTest by getting {
|
||||||
val jvmMain by getting {
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.undertow:undertow-core:2.3.14.Final")
|
implementation(kotlin("test"))
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val jvmTest by getting
|
||||||
val jsTest 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 {
|
pluginManagement {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
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"
|
rootProject.name = "markdown-parser"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package nl.astraeus.wiki.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.wiki.parser
|
||||||
|
|
||||||
|
import nl.astraeus.wiki.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.wiki.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.wiki.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,50 @@
|
|||||||
|
package nl.astraeus.markdown.parser
|
||||||
|
|
||||||
|
import nl.astraeus.wiki.parser.MarkdownPart
|
||||||
|
import nl.astraeus.wiki.parser.markdown
|
||||||
|
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,23 @@
|
|||||||
|
package nl.astraeus.markdown.parser
|
||||||
|
|
||||||
|
import nl.astraeus.wiki.parser.MarkdownPart
|
||||||
|
import nl.astraeus.wiki.parser.parseParagraph
|
||||||
|
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