From ab2572d95d9eb1289077f5f4619a1dc1314f615f Mon Sep 17 00:00:00 2001 From: rnentjes Date: Wed, 7 Jan 2026 14:56:58 +0100 Subject: [PATCH] Add support for checkbox lists, indented code blocks, and escaped characters. Extend tests for new Markdown features. Update project version to 1.0.4. --- .idea/artifacts/markdown_parser_js_1_0_1.xml | 8 +++ .idea/artifacts/markdown_parser_js_1_0_2.xml | 8 +++ .idea/artifacts/markdown_parser_js_1_0_3.xml | 8 +++ .../markdown_parser_js_1_0_4_SNAPSHOT.xml | 8 +++ .idea/artifacts/markdown_parser_jvm_1_0_1.xml | 8 +++ .idea/artifacts/markdown_parser_jvm_1_0_2.xml | 8 +++ .idea/artifacts/markdown_parser_jvm_1_0_3.xml | 8 +++ .../markdown_parser_jvm_1_0_4_SNAPSHOT.xml | 8 +++ build.gradle.kts | 2 +- .../nl/astraeus/markdown/parser/Markdown.kt | 16 ++++- .../nl/astraeus/markdown/parser/Paragraph.kt | 31 +++++++--- .../nl/astraeus/markdown/parser/Parser.kt | 60 ++++++++++++++++++- .../nl/astraeus/markdown/parser/ParseTest.kt | 32 ++++++++++ .../astraeus/markdown/parser/TestParagraph.kt | 49 +++++++++++++++ 14 files changed, 242 insertions(+), 12 deletions(-) create mode 100644 .idea/artifacts/markdown_parser_js_1_0_1.xml create mode 100644 .idea/artifacts/markdown_parser_js_1_0_2.xml create mode 100644 .idea/artifacts/markdown_parser_js_1_0_3.xml create mode 100644 .idea/artifacts/markdown_parser_js_1_0_4_SNAPSHOT.xml create mode 100644 .idea/artifacts/markdown_parser_jvm_1_0_1.xml create mode 100644 .idea/artifacts/markdown_parser_jvm_1_0_2.xml create mode 100644 .idea/artifacts/markdown_parser_jvm_1_0_3.xml create mode 100644 .idea/artifacts/markdown_parser_jvm_1_0_4_SNAPSHOT.xml diff --git a/.idea/artifacts/markdown_parser_js_1_0_1.xml b/.idea/artifacts/markdown_parser_js_1_0_1.xml new file mode 100644 index 0000000..2e75872 --- /dev/null +++ b/.idea/artifacts/markdown_parser_js_1_0_1.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_js_1_0_2.xml b/.idea/artifacts/markdown_parser_js_1_0_2.xml new file mode 100644 index 0000000..c779ae4 --- /dev/null +++ b/.idea/artifacts/markdown_parser_js_1_0_2.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_js_1_0_3.xml b/.idea/artifacts/markdown_parser_js_1_0_3.xml new file mode 100644 index 0000000..692f65a --- /dev/null +++ b/.idea/artifacts/markdown_parser_js_1_0_3.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_js_1_0_4_SNAPSHOT.xml b/.idea/artifacts/markdown_parser_js_1_0_4_SNAPSHOT.xml new file mode 100644 index 0000000..0edbfd2 --- /dev/null +++ b/.idea/artifacts/markdown_parser_js_1_0_4_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_jvm_1_0_1.xml b/.idea/artifacts/markdown_parser_jvm_1_0_1.xml new file mode 100644 index 0000000..c448d28 --- /dev/null +++ b/.idea/artifacts/markdown_parser_jvm_1_0_1.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_jvm_1_0_2.xml b/.idea/artifacts/markdown_parser_jvm_1_0_2.xml new file mode 100644 index 0000000..6648ff9 --- /dev/null +++ b/.idea/artifacts/markdown_parser_jvm_1_0_2.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_jvm_1_0_3.xml b/.idea/artifacts/markdown_parser_jvm_1_0_3.xml new file mode 100644 index 0000000..6fb99b3 --- /dev/null +++ b/.idea/artifacts/markdown_parser_jvm_1_0_3.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/markdown_parser_jvm_1_0_4_SNAPSHOT.xml b/.idea/artifacts/markdown_parser_jvm_1_0_4_SNAPSHOT.xml new file mode 100644 index 0000000..84fad01 --- /dev/null +++ b/.idea/artifacts/markdown_parser_jvm_1_0_4_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eab4917..5800fb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "nl.astraeus" -version = "1.0.3" +version = "1.0.4" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Markdown.kt b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Markdown.kt index d4d9d49..3a319a5 100644 --- a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Markdown.kt +++ b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Markdown.kt @@ -1,12 +1,18 @@ package nl.astraeus.markdown.parser +data class CheckboxItem( + val line: Int, + val checked: Boolean, + val text: String, +) + sealed class MarkdownPart { data object NewLine : MarkdownPart() data object PageBreak : MarkdownPart() - sealed class ParagraphPart() { + sealed class ParagraphPart { data class Text( val text: String ) : ParagraphPart() @@ -44,6 +50,10 @@ sealed class MarkdownPart { class InlineCode( val text: String ) : ParagraphPart() + + data class IndentedCode( + val code: String + ) : ParagraphPart() } data class Paragraph( @@ -59,6 +69,10 @@ sealed class MarkdownPart { val lines: List, ) : MarkdownPart() + data class CheckboxList( + val lines: List, + ) : MarkdownPart() + data class OrderedList( val lines: List, ) : MarkdownPart() diff --git a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt index 4bb4430..01c773c 100644 --- a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt +++ b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Paragraph.kt @@ -4,6 +4,7 @@ import nl.astraeus.markdown.parser.MarkdownPart.ParagraphPart.* private enum class ParType { TEXT, + CODE, LINK_LABEL, LINK_URL, LINK_TITLE, @@ -30,6 +31,14 @@ private data class ParState( ) 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 ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data -> Text(data[ParType.TEXT]!!) @@ -103,14 +112,7 @@ private val states = listOf( 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]!!) + Italic(data[ParType.ITALIC]!!) }, ) @@ -125,9 +127,22 @@ fun parseParagraph(text: String): MarkdownPart.Paragraph { val data: ParagraphData = mutableMapOf() var index = 0 var activeStates = states.filter { it.fromType == type } + var escaped = false while (index < text.length) { 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) { if (state.fromType == type && text.test(index, state.text)) { data[state.fromType] = buffer.toString() diff --git a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt index 70f5574..9e9bae6 100644 --- a/src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt +++ b/src/commonMain/kotlin/nl/astraeus/markdown/parser/Parser.kt @@ -5,7 +5,9 @@ enum class MarkdownType { PARAGRAPH, ORDERED_LIST, UNORDERED_LIST, + CHECKBOX_LIST, TABLE, + INDENTED_CODE, } fun markdown(text: String): List { @@ -17,6 +19,7 @@ fun markdown(text: String): List { var index = 0 val buffer = StringBuilder() + val checkboxList = mutableListOf() fun parseBuffer() { if (buffer.isNotBlank()) { @@ -33,7 +36,7 @@ fun markdown(text: String): List { //println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}") when { type == MarkdownType.ORDERED_LIST -> { - if (!line.startsWith("${listIndex++}.")) { + if (!line.startsWith("${listIndex++}.") && !line.startsWith("-.")) { parseBuffer() continue } else { @@ -42,6 +45,22 @@ fun markdown(text: String): List { } } + 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 -> { if (!line.startsWith("- ") && !line.startsWith("* ") @@ -69,6 +88,16 @@ fun markdown(text: String): List { parseBuffer() } + type == MarkdownType.INDENTED_CODE -> { + if (!rawLine.startsWith(" ")) { + parseBuffer() + continue + } else { + buffer.append(line) + buffer.append("\n") + } + } + line.startsWith("```") -> { if (type != MarkdownType.CODE) { parseBuffer() @@ -86,7 +115,7 @@ fun markdown(text: String): List { continue } - line.startsWith("1.") -> { + line.startsWith("1.") || line.startsWith("-.") -> { parseBuffer() type = MarkdownType.ORDERED_LIST listIndex = 2 @@ -94,6 +123,18 @@ fun markdown(text: String): List { 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("* ") -> { parseBuffer() type = MarkdownType.UNORDERED_LIST @@ -120,6 +161,13 @@ fun markdown(text: String): List { parts.add(MarkdownPart.Header(headerText, headerLevel)) } + rawLine.startsWith(" ") -> { + parseBuffer() + type = MarkdownType.INDENTED_CODE + buffer.append(line) + buffer.append("\n") + } + line == "[break]" -> { parseBuffer() parts.add(MarkdownPart.PageBreak) @@ -148,6 +196,10 @@ private fun handleBuffer( listOf(MarkdownPart.CodeBlock(text, language)) } + MarkdownType.INDENTED_CODE -> { + listOf(MarkdownPart.CodeBlock(text, "block")) + } + MarkdownType.PARAGRAPH -> { listOf(parseParagraph(text)) } @@ -160,6 +212,10 @@ private fun handleBuffer( listOf(MarkdownPart.UnorderedList(text.lines())) } + MarkdownType.CHECKBOX_LIST -> { + error("Checkbox list is handled separately") + } + MarkdownType.TABLE -> { parseTable(text) } diff --git a/src/commonTest/kotlin/nl/astraeus/markdown/parser/ParseTest.kt b/src/commonTest/kotlin/nl/astraeus/markdown/parser/ParseTest.kt index 4388085..c2e8ce7 100644 --- a/src/commonTest/kotlin/nl/astraeus/markdown/parser/ParseTest.kt +++ b/src/commonTest/kotlin/nl/astraeus/markdown/parser/ParseTest.kt @@ -34,6 +34,38 @@ class ParseTest { 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) { for (part in md) { if (part is MarkdownPart.Paragraph) { diff --git a/src/commonTest/kotlin/nl/astraeus/markdown/parser/TestParagraph.kt b/src/commonTest/kotlin/nl/astraeus/markdown/parser/TestParagraph.kt index 8d3bb5b..ecafc81 100644 --- a/src/commonTest/kotlin/nl/astraeus/markdown/parser/TestParagraph.kt +++ b/src/commonTest/kotlin/nl/astraeus/markdown/parser/TestParagraph.kt @@ -19,6 +19,31 @@ class TestParagraph { 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 fun testLink() { @@ -30,4 +55,28 @@ class TestParagraph { 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) + } + }