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