From 5fd15c975b90962f2939753bd6c4f1ada390e132 Mon Sep 17 00:00:00 2001 From: usbharu Date: Sat, 16 Nov 2024 02:25:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20url=E3=81=A8=E7=94=BB=E5=83=8F=E3=81=AE?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/markdown/AstNode.kt | 14 +++ .../kotlin/dev/usbharu/markdown/Lexer.kt | 4 +- .../kotlin/dev/usbharu/markdown/Parser.kt | 98 +++++++++++++++---- .../dev/usbharu/markdown/PeekableIterator.kt | 4 +- .../kotlin/dev/usbharu/markdown/LexerTest.kt | 25 ++++- .../kotlin/dev/usbharu/markdown/ParserTest.kt | 58 +++++++++++ 6 files changed, 179 insertions(+), 24 deletions(-) diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt index 0bc0ba1..9b107fd 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/AstNode.kt @@ -52,6 +52,11 @@ sealed class AstNode { } sealed class InlineNode : AstNode(), QuotableNode + data class InlineNodes(val nodes: MutableList) : InlineNode() { + override fun print(): String { + return nodes.joinToString("") { it.print() } + } + } data class ItalicNode(val nodes: MutableList) : InlineNode() { override fun print(): String { return nodes.joinToString("", prefix = "*", postfix = "*") { it.print() } @@ -70,5 +75,14 @@ sealed class AstNode { return text } } + + data class ImageNode(val urlUrlNode: UrlNode) : InlineNode() + + data class UrlNode(val url: UrlUrlNode, val urlNameNode: UrlNameNode, val urlTitleNode: UrlTitleNode?) : + InlineNode() + + data class UrlUrlNode(val url: String) : InlineNode() + data class UrlTitleNode(val title: String) : InlineNode() + data class UrlNameNode(val name: String) : InlineNode() } diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt index 2d1ec3f..696169a 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/Lexer.kt @@ -315,7 +315,7 @@ class Lexer { val nextC = charIterator.peekOrNull() ?: return val nextC2 = iterator.peekOrNull() ?: return if (nextC != nextC2) { - tokens.add(Text(urlBuilder.toString())) + addText(tokens, urlBuilder.toString()) return } urlBuilder.append(nextC2) @@ -323,7 +323,7 @@ class Lexer { iterator.next() } if (urlBuilder.length == 1) { - tokens.add(Text(urlBuilder.toString())) //hだけのときはURLじゃないのでテキストとして追加 + addText(tokens, urlBuilder.toString()) //hだけのときはURLじゃないのでテキストとして追加 } else { while (iterator.hasNext() && (iterator.peekOrNull() ?.isWhitespace() != true && iterator.peekOrNull() != ')') diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt index 2372b97..bb978ae 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/Parser.kt @@ -14,24 +14,22 @@ class Parser { val nodes = mutableListOf() while (iterator.hasNext()) { val node = when (val next = iterator.next()) { - is Asterisk, is InlineCodeBlock, is Strike, is Text -> paragraph(next, iterator) - is Break -> null + is Asterisk, is InlineCodeBlock, is Strike, + is Text, is Whitespace, Exclamation, ParenthesesEnd, ParenthesesStart, + SquareBracketStart, SquareBracketEnd, is Url, is UrlTitle -> paragraph( + next, + iterator + ) + + is Break -> null //todo ただの改行と段落分けの改行のトークンを分ける is CheckBox -> TODO() is CodeBlock -> TODO() is CodeBlockLanguage -> TODO() - Exclamation -> TODO() is Header -> header(next, iterator) is Html -> TODO() is Token.List -> TODO() - ParenthesesEnd -> TODO() - ParenthesesStart -> TODO() is Quote -> TODO() is Separator -> separator(next, iterator) - SquareBracketEnd -> TODO() - SquareBracketStart -> TODO() - is Url -> TODO() - is UrlTitle -> TODO() - is Whitespace -> TODO() } if (node != null) { nodes.add(node) @@ -64,30 +62,96 @@ class Parser { iterator.print() val node = when (token) { is Asterisk -> asterisk(token, iterator) - Exclamation -> TODO() + Exclamation -> image(Exclamation, iterator) is InlineCodeBlock -> TODO() - ParenthesesEnd -> TODO() - ParenthesesStart -> TODO() - SquareBracketEnd -> TODO() - SquareBracketStart -> TODO() + ParenthesesEnd -> PlainText(")") + ParenthesesStart -> PlainText("(") + SquareBracketEnd -> PlainText("]") + SquareBracketStart -> url(SquareBracketStart, iterator) is Strike -> TODO() is Text -> plainText(token, iterator) is Url -> TODO() - is UrlTitle -> TODO() - is Whitespace -> TODO() + is UrlTitle -> PlainText("\"${token.title}\"") + is Whitespace -> whitespace(token, iterator) else -> TODO() } return mutableListOf(node) } + fun whitespace(token: Whitespace, iterator: PeekableTokenIterator): InlineNode { + return PlainText(token.whitespace.toString().repeat(token.count)) + } + fun plainText(token: Text, iterator: PeekableTokenIterator): PlainText { return PlainText(token.text) } + fun image(exclamation: Exclamation, iterator: PeekableTokenIterator): InlineNode { + val squareBracketStartToken = iterator.peekOrNull() + if (squareBracketStartToken !is SquareBracketStart) { + TODO() + } + val url = url(squareBracketStartToken, iterator) + if (url !is UrlNode) { + return InlineNodes(mutableListOf(PlainText("!"), url)) + } + return ImageNode(url) + } + + fun url(squareBracketStart: SquareBracketStart, iterator: PeekableTokenIterator): InlineNode { + val urlNameToken = iterator.peekOrNull() + if (urlNameToken !is Text) { + return PlainText("[") + } + val text = iterator.next() as Text //text + val urlName = urlName(urlNameToken, iterator) + if (iterator.peekOrNull() !is SquareBracketEnd) { + return InlineNodes(mutableListOf(PlainText("["), PlainText(text.text))) + } + iterator.skip() // ] + if (iterator.peekOrNull() !is ParenthesesStart) { + return InlineNodes(mutableListOf(PlainText("["), PlainText(text.text), PlainText("]"))) + } + iterator.skip() //( + if (iterator.peekOrNull() !is Url && iterator.peekOrNull() !is Text) { + return InlineNodes(mutableListOf(PlainText("[${text.text}]("))) + } + val textOrUrl = iterator.next() + val urlUrlNode = if (textOrUrl is Text) { + UrlUrlNode(textOrUrl.text) + } else if (textOrUrl is Url) { + UrlUrlNode(textOrUrl.url) + } else { + TODO() + } + val whitespace = if (iterator.peekOrNull() is Whitespace) { + val whitespace = iterator.next() as Whitespace + whitespace.whitespace.toString().repeat(whitespace.count) + } else { + "" + } + val urlTitle = if (iterator.peekOrNull() is UrlTitle) { + UrlTitleNode((iterator.next() as UrlTitle).title) + } else { + null + } + if (iterator.peekOrNull() !is ParenthesesEnd) { + return InlineNodes(mutableListOf(PlainText("[${text.text}](${urlTitle?.title.orEmpty()}$whitespace"))) + } + iterator.skip() + return UrlNode(urlUrlNode, urlName, urlTitle) + } + + fun urlName(text: Text, iterator: PeekableTokenIterator): UrlNameNode { + return UrlNameNode(text.text) + } + fun asterisk(token: Asterisk, iterator: PeekableTokenIterator): InlineNode { var count = token.count var node: InlineNode? = null + + //todo **a*を正しくパースできないので閉じカウンタ的なものを追加し、token.countと閉じカウンタが一致しない場合plaintextに置き換える while ((count > 0)) { if (count == 3) { val italicBold = italic(token, iterator, 3) diff --git a/library/src/commonMain/kotlin/dev/usbharu/markdown/PeekableIterator.kt b/library/src/commonMain/kotlin/dev/usbharu/markdown/PeekableIterator.kt index b40bea4..c0972d4 100644 --- a/library/src/commonMain/kotlin/dev/usbharu/markdown/PeekableIterator.kt +++ b/library/src/commonMain/kotlin/dev/usbharu/markdown/PeekableIterator.kt @@ -1,7 +1,5 @@ package dev.usbharu.markdown -import kotlin.collections.List - class PeekableCharIterator(private val charArray: CharArray) : Iterator { private var index = 0 override fun hasNext(): Boolean = index < charArray.size @@ -51,7 +49,7 @@ class PeekableTokenIterator(private val tokens: List) : Iterator { fun peekOrNull(offset: Int): Token? = tokens.getOrNull(index + offset) fun current(): Int = index - fun skip(count: Int = 0) { + fun skip(count: Int = 1) { index += count } diff --git a/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt b/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt index 241b925..1c95516 100644 --- a/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt +++ b/library/src/commonTest/kotlin/dev/usbharu/markdown/LexerTest.kt @@ -59,7 +59,7 @@ class LexerTest { println(actual) - assertContentEquals(listOf(Text("abcd"), Break(1), Text("efg"), Text("h")), actual) + assertContentEquals(listOf(Text("abcd"), Break(1), Text("efgh")), actual) } @Test @@ -70,7 +70,7 @@ class LexerTest { println(actual) - assertContentEquals(listOf(Text("abcd"), Break(2), Text("efg"), Text("h")), actual) + assertContentEquals(listOf(Text("abcd"), Break(2), Text("efgh")), actual) } @Test @@ -675,6 +675,27 @@ class LexerTest { ) } + @Test + fun 不正urlとタイトル() { + val lexer = Lexer() + + val actual = lexer.lex("[alt](../hoge.html \"example\")") + + println(actual) + + assertContentEquals( + listOf( + SquareBracketStart, + Text("alt"), + SquareBracketEnd, + ParenthesesStart, + Url("https://example.com"), + UrlTitle("example"), + ParenthesesEnd + ), 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 f90b0d7..c3eca2d 100644 --- a/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt +++ b/library/src/commonTest/kotlin/dev/usbharu/markdown/ParserTest.kt @@ -251,4 +251,62 @@ class ParserTest { ), actual ) } + + @Test + fun アスタリスク不正() { + val parser = Parser() + + val actual = parser.parse(listOf(Asterisk(2, '*'), Text("a"), Asterisk(1, '*'))) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + ParagraphNode(listOf(BoldNode(mutableListOf(PlainText("a"))))) + ) + ) + ), actual + ) + } + + @Test + fun url() { + val parser = Parser() + + val actual = parser.parse( + listOf( + SquareBracketStart, + Text("alt"), + SquareBracketEnd, + ParenthesesStart, + Url("https://example.com"), + UrlTitle("example"), + ParenthesesEnd + ) + ) + + println(actual) + println(actual.print()) + + assertEquals( + RootNode( + BodyNode( + listOf( + ParagraphNode( + listOf( + UrlNode( + UrlUrlNode("https://example.com"), + UrlNameNode("alt"), UrlTitleNode("example") + ) + + ) + ) + ) + ) + ), actual + ) + } } \ No newline at end of file