summaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt
blob: 86b92d2dc5dfd87c3d4983afcb9f137a3544ad58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package moe.nea.blog.md

import moe.nea.blog.util.indentSize
import java.util.*


class MarkdownParser(source: String) {
    private val lines = source.lines()
    private var lineIndex = 0
    private var blockIndents = 0
    private val indentStack = Stack<Int>()

    private val blockParsers = mutableListOf<BlockParser>()
    private val inlineParsers = mutableListOf<InlineParser>()

    fun findParserFor(line: String): BlockParser? {
        return blockParsers.filter { it.detect(line) }
            .maxByOrNull { it.prio }
    }

    fun readChildBlock(): MarkdownBlock? {
        val peek = peekLine() ?: return null
        val blockParser = findParserFor(peek) ?: ParagraphParser
        return blockParser.parse(this)
    }

    fun pushIndent(newIndent: Int) {
        require(newIndent > blockIndents)
        indentStack.push(blockIndents)
        blockIndents = newIndent
    }

    fun popIndent() {
        blockIndents = indentStack.pop()
    }

    fun consumeLine(): String? {
        val line = peekLine()
        if (line != null)
            lineIndex++
        return line
    }

    fun peekLine(): String? {
        if (lineIndex !in lines.indices) return null
        val line = lines[lineIndex]
        val indent = line.indentSize()
        if (indent != null && indent < blockIndents)
            return null
        return line.substring(blockIndents)
    }

    fun parseInlineTextOnce(lookback: MarkdownFormat, text: String): Pair<MarkdownFormat, String> {
        val parser = inlineParsers.find { it.detect(lookback, text) }
        if (parser != null)
            return parser.parse(this, text)
        require(!text.isEmpty()) // TODO handle empty string
        if (text[0] == ' ')
            return Pair(Whitespace(), text.substring(1))
        val nextSpecial = text.indexOfFirst { it in inlineParsers.flatMap { it.specialSyntax } || it == ' ' }
        return Pair(Word(text.substring(0, nextSpecial)), text.substring(nextSpecial))
    }

    fun parseInlineText(text: String): MarkdownFormat {
        val seq = mutableListOf<MarkdownFormat>()
        var remaining = text
        var lastToken: MarkdownFormat = Begin()
        while (remaining.isNotEmpty()) {
            val (tok, next) = parseInlineTextOnce(lastToken, remaining)
            seq.add(tok)
            lastToken = tok
            remaining = next
        }
        return collapseInlineFormat(seq)
    }

    fun collapseInlineFormat(sequence: List<MarkdownFormat>): MarkdownFormat {
        return FormatSequence(sequence)
    }

    fun readDocument(): Document {
        val list = mutableListOf<MarkdownBlock>()
        while (true) {
            val block = readChildBlock() ?: break
            list.add(block)
        }
        return Document(list)
    }

    fun addDefaultParsers() {
        blockParsers.add(CodeBlockParser)
        blockParsers.add(HeaderParser)
        inlineParsers.add(ItalicsParser)
    }
}