aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/parsers/Parser.kt
blob: af07ec532179c5a01474e1188dcf1d28e17930a0 (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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package org.jetbrains.dokka.base.parsers

import org.jetbrains.dokka.model.doc.*
import org.jetbrains.dokka.model.doc.Deprecated
import org.jetbrains.dokka.model.doc.Suppress

abstract class Parser {

    abstract fun parseStringToDocNode(extractedString: String): DocTag

    abstract fun preparse(text: String): String

    open fun parse(text: String): DocumentationNode =
        DocumentationNode(extractTagsToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) })

    open fun parseTagWithBody(tagName: String, content: String): TagWrapper =
        when (tagName) {
            "description" -> Description(parseStringToDocNode(content))
            "author" -> Author(parseStringToDocNode(content))
            "version" -> Version(parseStringToDocNode(content))
            "since" -> Since(parseStringToDocNode(content))
            "see" -> See(
                parseStringToDocNode(content.substringAfter(' ')),
                content.substringBefore(' '),
                null
            )
            "param" -> Param(
                parseStringToDocNode(content.substringAfter(' ')),
                content.substringBefore(' ')
            )
            "property" -> Property(
                parseStringToDocNode(content.substringAfter(' ')),
                content.substringBefore(' ')
            )
            "return" -> Return(parseStringToDocNode(content))
            "constructor" -> Constructor(parseStringToDocNode(content))
            "receiver" -> Receiver(parseStringToDocNode(content))
            "throws", "exception" -> Throws(
                parseStringToDocNode(content.substringAfter(' ')),
                content.substringBefore(' '),
                null
            )
            "deprecated" -> Deprecated(parseStringToDocNode(content))
            "sample" -> Sample(
                parseStringToDocNode(content.substringAfter(' ')),
                content.substringBefore(' ')
            )
            "suppress" -> Suppress(parseStringToDocNode(content))
            else -> CustomTagWrapper(parseStringToDocNode(content), tagName)
        }

    /**
     * KDoc parser from Kotlin compiler relies on a comment asterisk
     * So there is a mini parser here
     * TODO: at least to adapt [org.jetbrains.kotlin.kdoc.lexer.KDocLexer] to analyze KDoc without the asterisks and use it here
     */
    private fun extractTagsToListOfPairs(text: String): List<Pair<String, String>> =
        "description $text"
            .extractKDocSections()
            .map { content ->
                val contentWithEscapedAts = content.replace("\\@", "@")
                val (tag, body) = contentWithEscapedAts.split(" ", limit = 2)
                tag to body
            }

    /**
     * Ignore a doc tag inside code spans and blocks
     * @see org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
     */
    private fun CharSequence.extractKDocSections(delimiter: String = "\n@"): List<String> {
        var countOfBackticks = 0
        var countOfTildes = 0
        var countOfBackticksInOpeningFence = 0
        var countOfTildesInOpeningFence = 0

        var isInCode = false
        val result = mutableListOf<String>()
        var rangeStart = 0
        var rangeEnd = 0
        var currentOffset = 0
        while (currentOffset < length) {

            when (get(currentOffset)) {
                '`' -> {
                    countOfBackticks++
                    countOfTildes = 0
                }
                '~' -> {
                    countOfTildes++
                    countOfBackticks = 0
                }
                else -> {
                    if (isInCode) {
                        // The closing code fence must be at least as long as the opening fence
                        if(countOfBackticks >= countOfBackticksInOpeningFence
                                || countOfTildes >= countOfTildesInOpeningFence)
                            isInCode = false
                    } else {
                        // as per CommonMark spec, there can be any number of backticks for a code span, not only one or three
                        if (countOfBackticks > 0) {
                            isInCode = true
                            countOfBackticksInOpeningFence = countOfBackticks
                            countOfTildesInOpeningFence = Int.MAX_VALUE
                        }
                        // tildes are only for a code block, not code span
                        if (countOfTildes >= 3) {
                            isInCode = true
                            countOfTildesInOpeningFence = countOfTildes
                            countOfBackticksInOpeningFence = Int.MAX_VALUE
                        }
                    }
                    countOfTildes = 0
                    countOfBackticks = 0
                }
            }
            if (!isInCode && startsWith(delimiter, currentOffset)) {
                result.add(substring(rangeStart, rangeEnd))
                currentOffset += delimiter.length
                rangeStart = currentOffset
                rangeEnd = currentOffset
                continue
            }

            ++rangeEnd
            ++currentOffset
        }
        result.add(substring(rangeStart, rangeEnd))
        return result
    }

}