feat: urlと画像のパースを追加

This commit is contained in:
usbharu 2024-11-16 02:25:39 +09:00
parent ecff061924
commit 5fd15c975b
Signed by: usbharu
GPG Key ID: 95CBCF7046307B77
6 changed files with 179 additions and 24 deletions

View File

@ -52,6 +52,11 @@ sealed class AstNode {
}
sealed class InlineNode : AstNode(), QuotableNode
data class InlineNodes(val nodes: MutableList<InlineNode>) : InlineNode() {
override fun print(): String {
return nodes.joinToString("") { it.print() }
}
}
data class ItalicNode(val nodes: MutableList<InlineNode>) : 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()
}

View File

@ -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() != ')')

View File

@ -14,24 +14,22 @@ class Parser {
val nodes = mutableListOf<AstNode>()
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)

View File

@ -1,7 +1,5 @@
package dev.usbharu.markdown
import kotlin.collections.List
class PeekableCharIterator(private val charArray: CharArray) : Iterator<Char> {
private var index = 0
override fun hasNext(): Boolean = index < charArray.size
@ -51,7 +49,7 @@ class PeekableTokenIterator(private val tokens: List<Token>) : Iterator<Token> {
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
}

View File

@ -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()

View File

@ -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
)
}
}