Add support for checkbox lists, indented code blocks, and escaped characters. Extend tests for new Markdown features. Update project version to 1.0.4.

This commit is contained in:
2026-01-07 14:56:58 +01:00
parent 6d7d05a0d4
commit ab2572d95d
14 changed files with 242 additions and 12 deletions

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-1.0.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-1.0.1.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-1.0.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-1.0.2.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-1.0.3">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-1.0.3.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-1.0.4-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-1.0.4-SNAPSHOT.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-1.0.1">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-1.0.1.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-1.0.2">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-1.0.2.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-1.0.3">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-1.0.3.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-1.0.4-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-1.0.4-SNAPSHOT.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -8,7 +8,7 @@ plugins {
} }
group = "nl.astraeus" group = "nl.astraeus"
version = "1.0.3" version = "1.0.4"
repositories { repositories {
mavenCentral() mavenCentral()

View File

@@ -1,12 +1,18 @@
package nl.astraeus.markdown.parser package nl.astraeus.markdown.parser
data class CheckboxItem(
val line: Int,
val checked: Boolean,
val text: String,
)
sealed class MarkdownPart { sealed class MarkdownPart {
data object NewLine : MarkdownPart() data object NewLine : MarkdownPart()
data object PageBreak : MarkdownPart() data object PageBreak : MarkdownPart()
sealed class ParagraphPart() { sealed class ParagraphPart {
data class Text( data class Text(
val text: String val text: String
) : ParagraphPart() ) : ParagraphPart()
@@ -44,6 +50,10 @@ sealed class MarkdownPart {
class InlineCode( class InlineCode(
val text: String val text: String
) : ParagraphPart() ) : ParagraphPart()
data class IndentedCode(
val code: String
) : ParagraphPart()
} }
data class Paragraph( data class Paragraph(
@@ -59,6 +69,10 @@ sealed class MarkdownPart {
val lines: List<String>, val lines: List<String>,
) : MarkdownPart() ) : MarkdownPart()
data class CheckboxList(
val lines: List<CheckboxItem>,
) : MarkdownPart()
data class OrderedList( data class OrderedList(
val lines: List<String>, val lines: List<String>,
) : MarkdownPart() ) : MarkdownPart()

View File

@@ -4,6 +4,7 @@ import nl.astraeus.markdown.parser.MarkdownPart.ParagraphPart.*
private enum class ParType { private enum class ParType {
TEXT, TEXT,
CODE,
LINK_LABEL, LINK_LABEL,
LINK_URL, LINK_URL,
LINK_TITLE, LINK_TITLE,
@@ -30,6 +31,14 @@ private data class ParState(
) )
private val states = listOf( private val states = listOf(
// Inline Code
ParState(ParType.TEXT, "`", ParType.INLINE_CODE) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.INLINE_CODE, "`", ParType.TEXT) { data ->
InlineCode(data[ParType.INLINE_CODE]!!)
},
// Image with link // Image with link
ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data -> ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data ->
Text(data[ParType.TEXT]!!) Text(data[ParType.TEXT]!!)
@@ -103,14 +112,7 @@ private val states = listOf(
Text(data[ParType.TEXT]!!) Text(data[ParType.TEXT]!!)
}, },
ParState(ParType.ITALIC, "*", ParType.TEXT) { data -> ParState(ParType.ITALIC, "*", ParType.TEXT) { data ->
BoldItalic(data[ParType.ITALIC]!!) Italic(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]!!)
}, },
) )
@@ -125,9 +127,22 @@ fun parseParagraph(text: String): MarkdownPart.Paragraph {
val data: ParagraphData = mutableMapOf() val data: ParagraphData = mutableMapOf()
var index = 0 var index = 0
var activeStates = states.filter { it.fromType == type } var activeStates = states.filter { it.fromType == type }
var escaped = false
while (index < text.length) { while (index < text.length) {
var found = false var found = false
val ch = text[index]
if (!escaped && ch == '\\') {
escaped = true
index++
continue
} else if (escaped) {
escaped = false
buffer.append(ch)
index++
continue
}
for (state in activeStates) { for (state in activeStates) {
if (state.fromType == type && text.test(index, state.text)) { if (state.fromType == type && text.test(index, state.text)) {
data[state.fromType] = buffer.toString() data[state.fromType] = buffer.toString()

View File

@@ -5,7 +5,9 @@ enum class MarkdownType {
PARAGRAPH, PARAGRAPH,
ORDERED_LIST, ORDERED_LIST,
UNORDERED_LIST, UNORDERED_LIST,
CHECKBOX_LIST,
TABLE, TABLE,
INDENTED_CODE,
} }
fun markdown(text: String): List<MarkdownPart> { fun markdown(text: String): List<MarkdownPart> {
@@ -17,6 +19,7 @@ fun markdown(text: String): List<MarkdownPart> {
var index = 0 var index = 0
val buffer = StringBuilder() val buffer = StringBuilder()
val checkboxList = mutableListOf<CheckboxItem>()
fun parseBuffer() { fun parseBuffer() {
if (buffer.isNotBlank()) { if (buffer.isNotBlank()) {
@@ -33,7 +36,7 @@ fun markdown(text: String): List<MarkdownPart> {
//println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}") //println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}")
when { when {
type == MarkdownType.ORDERED_LIST -> { type == MarkdownType.ORDERED_LIST -> {
if (!line.startsWith("${listIndex++}.")) { if (!line.startsWith("${listIndex++}.") && !line.startsWith("-.")) {
parseBuffer() parseBuffer()
continue continue
} else { } else {
@@ -42,6 +45,22 @@ fun markdown(text: String): List<MarkdownPart> {
} }
} }
type == MarkdownType.CHECKBOX_LIST -> {
if (!line.startsWith("- [ ]") && !line.startsWith("- [x]")) {
parts.add(MarkdownPart.CheckboxList(checkboxList))
parseBuffer()
continue
} else {
checkboxList.add(
CheckboxItem(
index,
line.startsWith("- [x]"),
line.substring(5).trim()
)
)
}
}
type == MarkdownType.UNORDERED_LIST -> { type == MarkdownType.UNORDERED_LIST -> {
if (!line.startsWith("- ") && if (!line.startsWith("- ") &&
!line.startsWith("* ") !line.startsWith("* ")
@@ -69,6 +88,16 @@ fun markdown(text: String): List<MarkdownPart> {
parseBuffer() parseBuffer()
} }
type == MarkdownType.INDENTED_CODE -> {
if (!rawLine.startsWith(" ")) {
parseBuffer()
continue
} else {
buffer.append(line)
buffer.append("\n")
}
}
line.startsWith("```") -> { line.startsWith("```") -> {
if (type != MarkdownType.CODE) { if (type != MarkdownType.CODE) {
parseBuffer() parseBuffer()
@@ -86,7 +115,7 @@ fun markdown(text: String): List<MarkdownPart> {
continue continue
} }
line.startsWith("1.") -> { line.startsWith("1.") || line.startsWith("-.") -> {
parseBuffer() parseBuffer()
type = MarkdownType.ORDERED_LIST type = MarkdownType.ORDERED_LIST
listIndex = 2 listIndex = 2
@@ -94,6 +123,18 @@ fun markdown(text: String): List<MarkdownPart> {
buffer.append("\n") buffer.append("\n")
} }
line.startsWith("- [ ]") || line.startsWith("- [x]") -> {
parseBuffer()
type = MarkdownType.CHECKBOX_LIST
checkboxList.add(
CheckboxItem(
index,
line.startsWith("- [x]"),
line.substring(5).trim()
)
)
}
line.startsWith("- ") || line.startsWith("* ") -> { line.startsWith("- ") || line.startsWith("* ") -> {
parseBuffer() parseBuffer()
type = MarkdownType.UNORDERED_LIST type = MarkdownType.UNORDERED_LIST
@@ -120,6 +161,13 @@ fun markdown(text: String): List<MarkdownPart> {
parts.add(MarkdownPart.Header(headerText, headerLevel)) parts.add(MarkdownPart.Header(headerText, headerLevel))
} }
rawLine.startsWith(" ") -> {
parseBuffer()
type = MarkdownType.INDENTED_CODE
buffer.append(line)
buffer.append("\n")
}
line == "[break]" -> { line == "[break]" -> {
parseBuffer() parseBuffer()
parts.add(MarkdownPart.PageBreak) parts.add(MarkdownPart.PageBreak)
@@ -148,6 +196,10 @@ private fun handleBuffer(
listOf(MarkdownPart.CodeBlock(text, language)) listOf(MarkdownPart.CodeBlock(text, language))
} }
MarkdownType.INDENTED_CODE -> {
listOf(MarkdownPart.CodeBlock(text, "block"))
}
MarkdownType.PARAGRAPH -> { MarkdownType.PARAGRAPH -> {
listOf(parseParagraph(text)) listOf(parseParagraph(text))
} }
@@ -160,6 +212,10 @@ private fun handleBuffer(
listOf(MarkdownPart.UnorderedList(text.lines())) listOf(MarkdownPart.UnorderedList(text.lines()))
} }
MarkdownType.CHECKBOX_LIST -> {
error("Checkbox list is handled separately")
}
MarkdownType.TABLE -> { MarkdownType.TABLE -> {
parseTable(text) parseTable(text)
} }

View File

@@ -34,6 +34,38 @@ class ParseTest {
printMarkdownParts(md) printMarkdownParts(md)
} }
@Test
fun testIndentedCode() {
val input = """
Dit is een text
Code block
Code block
Meer text
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
@Test
fun testCheckboxList() {
val input = """
Dit is een text
- [ ] Not checked
- [x] Checked
Meer text
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
private fun printMarkdownParts(md: List<MarkdownPart>) { private fun printMarkdownParts(md: List<MarkdownPart>) {
for (part in md) { for (part in md) {
if (part is MarkdownPart.Paragraph) { if (part is MarkdownPart.Paragraph) {

View File

@@ -19,6 +19,31 @@ class TestParagraph {
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text) assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
} }
@Test
fun testCode() {
val input = "Text `code` 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.InlineCode)
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
}
@Test
fun testIndentedCode() {
val input = "Text \n code\n line 2\n\nText"
val result = parseParagraph(input)
assertEquals(3, result.parts.size)
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
assertTrue(result.parts[1] is MarkdownPart.ParagraphPart.IndentedCode)
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
}
@Test @Test
fun testLink() { fun testLink() {
@@ -30,4 +55,28 @@ class TestParagraph {
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Link) assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Link)
} }
@Test
fun testEscaped() {
val input = "Test \\`escaped\\` text"
val result = parseParagraph(input)
assertEquals(1, result.parts.size)
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
}
@Test
fun testFormatting() {
val input = "test **bold**, *italic*, ***bold and italic***, ~~strikethrough~~, and `code`."
val result = parseParagraph(input)
assertEquals(1, result.parts.size)
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
}
} }