xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt (revision 5be3f65c8cf0e6db0a7e312df5006e8e93cdf9ec)
1 /*
2  * Copyright (c) Tor Norbye.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.facebook.ktfmt.kdoc
18 
19 import kotlin.math.min
20 
21 /** Formatter which can reformat KDoc comments. */
22 class KDocFormatter(private val options: KDocFormattingOptions) {
23   /** Reformats the [comment], which follows the given [initialIndent] string. */
reformatCommentnull24   fun reformatComment(comment: String, initialIndent: String): String {
25     return reformatComment(FormattingTask(options, comment, initialIndent))
26   }
27 
reformatCommentnull28   fun reformatComment(task: FormattingTask): String {
29     val indent = task.secondaryIndent
30     val indentSize = getIndentSize(indent, options)
31     val firstIndentSize = getIndentSize(task.initialIndent, options)
32     val comment = task.comment
33     val lineComment = comment.isLineComment()
34     val blockComment = comment.isBlockComment()
35     val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize)
36     val commentType = task.type
37     val lineSeparator = "\n$indent${commentType.linePrefix}"
38     val prefix = commentType.prefix
39 
40     // Collapse single line? If alternate is turned on, use the opposite of the
41     // setting
42     val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it }
43     if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) {
44       // Does the text fit on a single line?
45       val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: ""
46       // Subtract out space for "/** " and " */" and the indent:
47       val width =
48           min(
49               options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(),
50               options.maxCommentWidth)
51       val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}"
52       if (trimmed.length <= width) {
53         return "$prefix $trimmed$suffix"
54       }
55       if (indentSize < firstIndentSize) {
56         val nextLineWidth =
57             min(
58                 options.maxLineWidth - indentSize - commentType.singleLineOverhead(),
59                 options.maxCommentWidth)
60         if (trimmed.length <= nextLineWidth) {
61           return "$prefix $trimmed$suffix"
62         }
63       }
64     }
65 
66     val sb = StringBuilder()
67 
68     sb.append(prefix)
69     if (lineComment) {
70       sb.append(' ')
71     } else {
72       sb.append(lineSeparator)
73     }
74 
75     for (paragraph in paragraphs) {
76       if (paragraph.separate) {
77         // Remove trailing spaces which can happen when we have a paragraph
78         // separator
79         stripTrailingSpaces(lineComment, sb)
80         sb.append(lineSeparator)
81       }
82       val text = paragraph.text
83       if (paragraph.preformatted || paragraph.table) {
84         sb.append(text)
85         // Remove trailing spaces which can happen when we have an empty line in a
86         // preformatted paragraph.
87         stripTrailingSpaces(lineComment, sb)
88         sb.append(lineSeparator)
89         continue
90       }
91 
92       val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead()
93       val quoteAdjustment = if (paragraph.quoted) 2 else 0
94       val maxLineWidth =
95           min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment
96       val firstMaxLineWidth =
97           if (sb.indexOf('\n') == -1) {
98             min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment
99           } else {
100             maxLineWidth
101           }
102 
103       val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth)
104       var first = true
105       val hangingIndent = paragraph.hangingIndent
106       for (line in lines) {
107         sb.append(paragraph.indent)
108         if (first && !paragraph.continuation) {
109           first = false
110         } else {
111           sb.append(hangingIndent)
112         }
113         if (paragraph.quoted) {
114           sb.append("> ")
115         }
116         if (line.isEmpty()) {
117           // Remove trailing spaces which can happen when we have a paragraph
118           // separator
119           stripTrailingSpaces(lineComment, sb)
120         } else {
121           sb.append(line)
122         }
123         sb.append(lineSeparator)
124       }
125     }
126     if (!lineComment) {
127       if (sb.endsWith("* ")) {
128         sb.setLength(sb.length - 2)
129       }
130       sb.append("*/")
131     } else if (sb.endsWith(lineSeparator)) {
132       @Suppress("ReturnValueIgnored") sb.removeSuffix(lineSeparator)
133     }
134 
135     val formatted =
136         if (lineComment) {
137           sb.trim().removeSuffix("//").trim().toString()
138         } else if (blockComment) {
139           sb.toString().replace(lineSeparator + "\n", "\n\n")
140         } else {
141           sb.toString()
142         }
143 
144     val separatorIndex = comment.indexOf('\n')
145     return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') {
146       // CRLF separator
147       formatted.replace("\n", "\r\n")
148     } else {
149       formatted
150     }
151   }
152 
stripTrailingSpacesnull153   private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) {
154     if (!lineComment && sb.endsWith("* ")) {
155       sb.setLength(sb.length - 1)
156     } else if (lineComment && sb.endsWith("// ")) {
157       sb.setLength(sb.length - 1)
158     }
159   }
160 }
161