1 /* <lambda>null2 * 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.max 36 37 class Table( 38 private val columns: Int, 39 private val widths: List<Int>, 40 private val rows: List<Row>, 41 private val align: List<Align>, 42 private val original: List<String> 43 ) { 44 fun original(): List<String> { 45 return original 46 } 47 48 /** 49 * Format the table. Note that table rows cannot be broken into multiple lines in Markdown tables, 50 * so the [maxWidth] here is used to decide whether to add padding around the table only, and it's 51 * quite possible for the table to format to wider lengths than [maxWidth]. 52 */ 53 fun format(maxWidth: Int = Integer.MAX_VALUE): List<String> { 54 val tableMaxWidth = 55 2 + widths.sumOf { it + 2 } // +2: "| " in each cell and final " |" on the right 56 57 val pad = tableMaxWidth <= maxWidth 58 val lines = mutableListOf<String>() 59 for (i in rows.indices) { 60 val sb = StringBuilder() 61 val row = rows[i] 62 for (column in 0 until row.cells.size) { 63 sb.append('|') 64 if (pad) { 65 sb.append(' ') 66 } 67 val cell = row.cells[column] 68 val width = widths[column] 69 val s = 70 if (align[column] == Align.CENTER && i > 0) { 71 String.format( 72 "%-${width}s", 73 String.format("%${cell.length + (width - cell.length) / 2}s", cell)) 74 } else if (align[column] == Align.RIGHT && i > 0) { 75 String.format("%${width}s", cell) 76 } else { 77 String.format("%-${width}s", cell) 78 } 79 sb.append(s) 80 if (pad) { 81 sb.append(' ') 82 } 83 } 84 sb.append('|') 85 lines.add(sb.toString()) 86 sb.clear() 87 88 if (i == 0) { 89 for (column in 0 until row.cells.size) { 90 sb.append('|') 91 var width = widths[column] 92 if (align[column] != Align.LEFT) { 93 width-- 94 if (align[column] == Align.CENTER) { 95 sb.append(':') 96 width-- 97 } 98 } 99 if (pad) { 100 sb.append('-') 101 } 102 val s = "-".repeat(width) 103 sb.append(s) 104 if (pad) { 105 sb.append('-') 106 } 107 if (align[column] != Align.LEFT) { 108 sb.append(':') 109 } 110 } 111 sb.append('|') 112 lines.add(sb.toString()) 113 sb.clear() 114 } 115 } 116 117 return lines 118 } 119 120 companion object { 121 /** 122 * If the line starting at index [start] begins a table, return that table as well as the index 123 * of the first line after the table. 124 */ 125 fun getTable( 126 lines: List<String>, 127 start: Int, 128 lineContent: (String) -> String 129 ): Pair<Table, Int>? { 130 if (start > lines.size - 2) { 131 return null 132 } 133 val headerLine = lineContent(lines[start]) 134 val separatorLine = lineContent(lines[start + 1]) 135 val barCount = countSeparators(headerLine) 136 if (!isHeaderDivider(barCount, separatorLine.trim())) { 137 return null 138 } 139 val header = getRow(headerLine) ?: return null 140 val rows = mutableListOf<Row>() 141 rows.add(header) 142 143 val dividerRow = getRow(separatorLine) ?: return null 144 145 var i = start + 2 146 while (i < lines.size) { 147 val line = lineContent(lines[i]) 148 if (!line.contains("|")) { 149 break 150 } 151 val row = getRow(line) ?: break 152 rows.add(row) 153 i++ 154 } 155 156 val rowsAndDivider = rows + dividerRow 157 if (rowsAndDivider.all { row -> 158 val first = row.cells.firstOrNull() 159 first != null && first.isBlank() 160 }) { 161 rowsAndDivider.forEach { if (it.cells.isNotEmpty()) it.cells.removeAt(0) } 162 } 163 164 // val columns = rows.maxOf { it.cells.size } 165 val columns = dividerRow.cells.size 166 val maxColumns = rows.maxOf { it.cells.size } 167 val widths = mutableListOf<Int>() 168 for (column in 0 until maxColumns) { 169 widths.add(3) 170 } 171 for (row in rows) { 172 for (column in 0 until row.cells.size) { 173 widths[column] = max(widths[column], row.cells[column].length) 174 } 175 for (column in row.cells.size until columns) { 176 row.cells.add("") 177 } 178 } 179 180 val align = mutableListOf<Align>() 181 for (cell in dividerRow.cells) { 182 val direction = 183 if (cell.endsWith(":")) { 184 if (cell.startsWith(":-")) { 185 Align.CENTER 186 } else { 187 Align.RIGHT 188 } 189 } else { 190 Align.LEFT 191 } 192 align.add(direction) 193 } 194 for (column in align.size until maxColumns) { 195 align.add(Align.LEFT) 196 } 197 val table = 198 Table(columns, widths, rows, align, lines.subList(start, i).map { lineContent(it) }) 199 return Pair(table, i) 200 } 201 202 /** Returns true if the given String looks like a markdown table header divider. */ 203 private fun isHeaderDivider(barCount: Int, s: String): Boolean { 204 var i = 0 205 var count = 0 206 while (i < s.length) { 207 val c = s[i++] 208 if (c == '\\') { 209 i++ 210 } else if (c == '|') { 211 count++ 212 } else if (c.isWhitespace() || c == ':') { 213 continue 214 } else if (c == '-' && 215 (s.startsWith("--", i) || 216 s.startsWith("-:", i) || 217 (i > 1 && s.startsWith(":-:", i - 2)) || 218 (i > 1 && s.startsWith(":--", i - 2)))) { 219 while (i < s.length && s[i] == '-') { 220 i++ 221 } 222 } else { 223 return false 224 } 225 } 226 227 return barCount == count 228 } 229 230 private fun getRow(s: String): Row? { 231 // Can't just use String.split('|') because that would not handle escaped |'s 232 if (s.indexOf('|') == -1) { 233 return null 234 } 235 val row = Row() 236 var i = 0 237 var end = 0 238 while (end < s.length) { 239 val c = s[end] 240 if (c == '\\') { 241 end++ 242 } else if (c == '|') { 243 val cell = s.substring(i, end).trim() 244 if (end > 0) { 245 row.cells.add(cell.trim()) 246 } 247 i = end + 1 248 } 249 end++ 250 } 251 if (end > i) { 252 val cell = s.substring(i, end).trim() 253 if (cell.isNotEmpty()) { 254 row.cells.add(cell.trim()) 255 } 256 } 257 258 return row 259 } 260 261 private fun countSeparators(s: String): Int { 262 var i = 0 263 var count = 0 264 while (i < s.length) { 265 val c = s[i] 266 if (c == '|') { 267 count++ 268 } else if (c == '\\') { 269 i++ 270 } 271 i++ 272 } 273 return count 274 } 275 } 276 277 enum class Align { 278 LEFT, 279 RIGHT, 280 CENTER 281 } 282 283 class Row { 284 val cells: MutableList<String> = mutableListOf() 285 } 286 } 287