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