• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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