diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt index c2fb37f..933e8ff 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt @@ -16,7 +16,13 @@ sealed class AstNode { data class BodyNode(val body: List) : AstNode() { override fun print(): String { - return body.joinToString("\n") { it.print() } + return body.joinToString("") { + if (it is BlockNode) { + it.print() + "\n" + } else { + it.print() + } + } } } @@ -34,8 +40,30 @@ sealed class AstNode { } } - sealed interface QuotableNode - data class QuoteNode(val nodes: List) : AstNode(), QuotableNode + sealed interface QuotableNode { + fun print(): String + } + + data class QuoteNode(val nodes: MutableList) : AstNode(), QuotableNode { + override fun print(): String { + return printNest(1) + } + + fun printNest(nest: Int): String { + val builder = StringBuilder() + for (node in nodes) { + if (node is QuoteNode) { + builder.append(node.printNest(nest + 1)) + } else if (node is BreakNode) { + builder.append(node.print()) + } else { + builder.append(">".repeat(nest)).append(' ').append(node.print()) + } + } + return builder.toString() + } + } + data object SeparatorNode : BlockNode() { override fun print(): String { return "---" @@ -57,6 +85,7 @@ sealed class AstNode { return nodes.joinToString("") { it.print() } } } + data class ItalicNode(val nodes: MutableList) : InlineNode() { override fun print(): String { return nodes.joinToString("", prefix = "*", postfix = "*") { it.print() } @@ -118,5 +147,11 @@ sealed class AstNode { return url } } + + data object BreakNode : InlineNode() { + override fun print(): String { + return "\n" + } + } } diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt index eb553aa..837ddf7 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt @@ -89,7 +89,7 @@ class Lexer { } else if (htmlNest != 0) { codeBuffer.append(" ") } else { - addBreak(tokens) + addBreak(tokens, inQuote) } } inQuote = false @@ -107,6 +107,9 @@ class Lexer { tokens.removeLast() tokens.add(LineBreak(1)) } + if (lastToken is InQuoteBreak) { + tokens.removeLast() + } return tokens } @@ -527,7 +530,11 @@ class Lexer { } } - fun addBreak(tokens: MutableList) { + fun addBreak(tokens: MutableList, inQuote: Boolean = false) { + if (inQuote) { + tokens.add(InQuoteBreak) + return + } val lastOrNull = tokens.lastOrNull() if (lastOrNull is LineBreak && 1 <= lastOrNull.count) { tokens.removeLast() diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt index 21acf07..76a7dbe 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt @@ -28,8 +28,9 @@ class Parser { is Header -> header(next, iterator) is Html -> TODO() is Token.List -> TODO() - is Quote -> TODO() + is Quote -> quote(next, iterator) is Separator -> separator(next, iterator) + InQuoteBreak -> TODO() } if (node != null) { nodes.add(node) @@ -38,6 +39,44 @@ class Parser { return RootNode(BodyNode(nodes)) } + tailrec fun addQuote(quoteNode: QuoteNode, quotableNode: List, nest: Int) { + if (nest == 1) { + quoteNode.nodes.addAll(quotableNode) + return + } + addQuote(quoteNode.nodes.findLast { it is QuoteNode } as QuoteNode, quotableNode, nest.dec()) + } + + fun quote(quote: Quote, iterator: PeekableTokenIterator): QuoteNode { + var quote2 = quote + var maxNest = quote.count + val quoteNode = createNest(maxNest) + while (true) { + val list = mutableListOf() + while (isInline(iterator.peekOrNull()) && iterator.peekOrNull() !is InQuoteBreak) { + println("next token: " + iterator.peekOrNull()) + list.addAll(inline(iterator.next(), iterator)) + } + if (iterator.peekOrNull() is InQuoteBreak) { + list.add(BreakNode) + iterator.skip() + } + if (maxNest < quote2.count) { + addQuote(quoteNode, mutableListOf(createNest(quote2.count - maxNest)), maxNest) + maxNest = quote2.count + } + addQuote(quoteNode, list, quote2.count) + if (iterator.peekOrNull() is Quote) { + quote2 = iterator.next() as Quote + } else { + break + } + + } + return quoteNode + } + + fun separator(separator: Separator, iterator: PeekableTokenIterator): AstNode { return SeparatorNode } @@ -74,7 +113,7 @@ class Parser { return when (token) { is Asterisk, is InlineCodeBlock, is Strike, is Text, is Whitespace, Exclamation, ParenthesesEnd, ParenthesesStart, - SquareBracketStart, SquareBracketEnd, is Url, is UrlTitle, is LineBreak -> true + SquareBracketStart, SquareBracketEnd, is Url, is UrlTitle, is LineBreak, is InQuoteBreak -> true else -> false } @@ -97,7 +136,10 @@ class Parser { is UrlTitle -> PlainText("\"${token.title}\"") is Whitespace -> whitespace(token, iterator) is LineBreak -> null - else -> TODO() + else -> { + println("error" + token) + TODO() + } } if (node != null) { @@ -271,4 +313,20 @@ class Parser { iterator.print() return null } + + companion object { + fun createNest(nest: Int, quoteNode: QuoteNode = QuoteNode(mutableListOf())): QuoteNode { + createNest2(nest, quoteNode, quoteNode) + return quoteNode + } + + tailrec fun createNest2(nest: Int, current: QuoteNode, quoteNode: QuoteNode): QuoteNode { + if (nest == 1) { + return quoteNode + } + val element = QuoteNode(mutableListOf()) + current.nodes.add(element) + return createNest2(nest - 1, element, quoteNode) + } + } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/Token.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/Token.kt index d155a2e..2ea2eeb 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/Token.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/Token.kt @@ -7,6 +7,7 @@ sealed class Token { data class Text(var text: String) : Token() data class LineBreak(var count: Int) : Token() data object BlockBreak : Token() + data object InQuoteBreak : Token() data class Header(var count: Int) : Token() data class Quote(var count: Int) : Token() data class Separator(var count: Int, val char: Char) : Token() diff --git a/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt b/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt index 92fa9e3..562916f 100644 --- a/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt +++ b/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt @@ -219,6 +219,30 @@ class LexerTest { assertContentEquals(listOf(Quote(2), Text(">abcd")), actual) } + @Test + fun 引用複数行() { + val lexer = Lexer() + + val actual = lexer.lex("> aiueo\n>> >abcd\n> hoge\nfuga") + + println(actual) + + assertContentEquals( + listOf( + Quote(1), + Text("aiueo"), + InQuoteBreak, + Quote(2), + Text(">abcd"), + InQuoteBreak, + Quote(1), + Text("hoge"), + InQuoteBreak, + Text("fuga") + ), actual + ) + } + @Test fun セパレーター() { val lexer = Lexer() diff --git a/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt b/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt index 48dfca4..39b5b88 100644 --- a/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt +++ b/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt @@ -343,4 +343,162 @@ class ParserTest { ), actual ) } + + @Test + fun createNest() { + val quoteNode = QuoteNode(mutableListOf(PlainText("aa"))) + println(Parser.createNest(3, quoteNode)) + println(quoteNode) + } + + @Test + fun createNest2() { + val quoteNode = QuoteNode(mutableListOf(PlainText("aaa"))) + Parser.createNest2(3, quoteNode, quoteNode) + println(quoteNode) + } + + @Test + fun quote() { + val parser = Parser() + + val actual = parser.parse( + listOf( + Quote(1), Text("a") + ) + ) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + QuoteNode( + mutableListOf( + PlainText("a") + ) + ) + ) + ) + ), actual + ) + } + + @Test + fun quote2() { + val parser = Parser() + + val actual = parser.parse( + listOf( + Quote(2), Text("a") + ) + ) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + QuoteNode( + mutableListOf( + QuoteNode( + mutableListOf( + PlainText("a") + ) + ) + ) + ) + ) + ) + ), actual + ) + } + + @Test + fun quote3() { + val parser = Parser() + + val actual = parser.parse( + listOf( + Quote(2), Text("a"), InQuoteBreak, Quote(2), Text("abcd") + ) + ) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + QuoteNode( + mutableListOf( + QuoteNode( + mutableListOf( + PlainText("a"), BreakNode, PlainText("abcd") + ) + ) + ) + ) + ) + ) + ), actual + ) + } + + @Test + fun quote4() { + val parser = Parser() + + val actual = parser.parse( + listOf( + Quote(1), + Text("aiueo"), + InQuoteBreak, + Quote(2), + Text(">abcd"), + InQuoteBreak, + Quote(1), + Text("hoge"), + InQuoteBreak, + Text("fuga") + ) + ) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + QuoteNode( + mutableListOf( + PlainText("aiueo"), + BreakNode, + QuoteNode( + mutableListOf( + PlainText(">abcd"), + BreakNode + ) + ), + PlainText("hoge"), + BreakNode + ) + ), + ParagraphNode( + mutableListOf( + PlainText("fuga") + ) + ) + + ) + ) + ), actual + ) + } } \ No newline at end of file