├── .editorconfig ├── .github ├── renovate.json5 └── workflows │ ├── .java-version │ └── build.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── picnic ├── build.gradle ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── picnic │ │ ├── IntCounts.kt │ │ ├── dsl.kt │ │ ├── model.kt │ │ ├── textBorder.kt │ │ ├── textLayout.kt │ │ └── textRender.kt │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── picnic │ │ ├── CellAlignmentTest.kt │ │ ├── CellBorderTest.kt │ │ ├── CellSizeTest.kt │ │ ├── CellSpanTest.kt │ │ ├── DslTest.kt │ │ ├── RepresentativeKotlinDslTest.kt │ │ └── TableBorderTest.kt │ └── jvmTest │ └── java │ └── com │ └── jakewharton │ └── picnic │ └── RepresentativeJavaBuilderTest.java ├── sample ├── README.md ├── build.gradle └── src │ └── main │ └── java │ └── example │ └── Main.kt └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | indent_size=2 3 | continuation_indent_size=2 4 | insert_final_newline=true 5 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ], 6 | ignorePresets: [ 7 | // Ensure we get the latest version and are not pinned to old versions. 8 | 'workarounds:javaLTSVersions', 9 | ], 10 | customManagers: [ 11 | // Update .java-version file with the latest JDK version. 12 | { 13 | customType: 'regex', 14 | fileMatch: [ 15 | '\\.java-version$', 16 | ], 17 | matchStrings: [ 18 | '(?.*)\\n', 19 | ], 20 | datasourceTemplate: 'java-version', 21 | depNameTemplate: 'java', 22 | // Only write the major version. 23 | extractVersionTemplate: '^(?\\d+)', 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/.java-version: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: {} 5 | workflow_dispatch: {} 6 | push: 7 | branches: 8 | - 'trunk' 9 | tags: 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version-file: .github/workflows/.java-version 22 | 23 | - uses: gradle/actions/wrapper-validation@v4 24 | - run: ./gradlew build 25 | 26 | - name: Extract release notes 27 | id: release_notes 28 | if: startsWith(github.ref, 'refs/tags/') 29 | uses: ffurrer2/extract-release-notes@v2 30 | 31 | - name: Create Release 32 | if: startsWith(github.ref, 'refs/tags/') 33 | uses: softprops/action-gh-release@v2 34 | with: 35 | body: ${{ steps.release_notes.outputs.release_notes }} 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Publish Artifacts 40 | run: ./gradlew publish 41 | if: github.ref == 'refs/heads/trunk' 42 | env: 43 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} 44 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | /.idea 3 | *.iml 4 | 5 | # Gradle 6 | /.gradle 7 | build 8 | /reports 9 | 10 | # Kotlin 11 | .kotlin 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - In-development snapshots are now published to the Central Portal Snapshots repository at https://central.sonatype.com/repository/maven-snapshots/. 8 | 9 | 10 | ## 0.7.0 - 2022-08-31 11 | 12 | ### Added 13 | 14 | * New Kotlin targets: 15 | * `androidNativeArm32` 16 | * `androidNativeArm64` 17 | * `androidNativeX86` 18 | * `androidNativeX64` 19 | * `linuxArm64` 20 | * `wasm` 21 | * `watchosDeviceArm64` 22 | 23 | ### Changed 24 | 25 | * Publish using a newer version of Kotlin to produce non-legacy artifacts. 26 | 27 | 28 | ## 0.6.0 - 2022-02-25 29 | 30 | * Library is now fully multiplatform supporting JS and native targets in addition to JVM/Android. 31 | * Text drawing is now provided by [crossword](https://github.com/JakeWharton/crossword) library. 32 | 33 | 34 | ## 0.5.0 - 2020-09-16 35 | 36 | * New: Each line in a multi-line cell is now individually aligned. 37 | * New: Do not measure ANSI control sequences allowing color to be used inside cells. 38 | 39 | 40 | ## 0.4.0 - 2020-08-12 41 | 42 | * New: `cells()` function adds multiple cells at once to a row. 43 | * `TextBorder` property name changed to `characters`. 44 | 45 | 46 | ## 0.3.1 - 2020-04-20 47 | 48 | * Fix: Use own implementation of codepoint iteration which enables running on Android. 49 | 50 | 51 | ## 0.3.0 - 2020-02-07 52 | 53 | * New: Support for a table-level border on the table style. This border is automatically collapsed 54 | with cell borders which are on the edge of the table. The table border makes it easy to enclose 55 | a table in a border without having to track first/last row/col. 56 | * Fix: Switch to using codepoints rather than characters as a primitive. As a result, multi-char 57 | glyphs which render as a single visual "character" are now supported and measured correctly. 58 | 59 | 60 | ## 0.2.0 - 2019-12-09 61 | 62 | * New: Builder-based API for Java users. 63 | * Fix: Use actual Java default methods on interfaces instead of fake Kotlin default methods. 64 | * Fix: Remove `data` modifier and public constructors from model types. Java users should use the 65 | new builders to create instances. Kotlin users should use the factory DSL. Kotlin users can no 66 | longer destructure or `copy` model types. 67 | 68 | 69 | ## 0.1.0 2019-11-18 70 | 71 | Initial release. 72 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Picnic Tables 2 | ============= 3 | 4 | A Kotlin DSL and Java/Kotlin builder API for constructing HTML-like tables 5 | which can be rendered to text. 6 | 7 | Features: 8 | 9 | * Borders (with multiple styles) 10 | * Padding (left/right/top/bottom) 11 | * Per-table, row, and cell styling 12 | * Header/footer sections 13 | * Row/column spans 14 | * Text alignment 15 | 16 | DSL: 17 | 18 | ```kotlin 19 | table { 20 | row("Hello", "World") 21 | row("Hola", "Mundo") 22 | } 23 | ``` 24 | ``` 25 | HelloWorld 26 | Hola Mundo 27 | ``` 28 | 29 | Very underwhelming! Picnic tables start completely unstyled by design. 30 | 31 | Jump to a [real-world example](#real-world-example) or continue reading for individual features. 32 | 33 | _(Note: these examples do not render correctly on mobile)_ 34 | 35 | ### Border 36 | 37 | ```kotlin 38 | table { 39 | cellStyle { 40 | // These options affect the style of all cells contained within the table. 41 | border = true 42 | } 43 | row("Hello", "World") 44 | row("Hola", "Mundo") 45 | } 46 | ``` 47 | ``` 48 | ┌─────┬─────┐ 49 | │Hello│World│ 50 | ├─────┼─────┤ 51 | │Hola │Mundo│ 52 | └─────┴─────┘ 53 | ``` 54 | 55 | ### Table Border Style 56 | 57 | ```kotlin 58 | table { 59 | style { 60 | // Unlike cellStyle, these options affect the style of the table itself. 61 | borderStyle = Hidden 62 | } 63 | cellStyle { 64 | border = true 65 | } 66 | row("Hello", "World") 67 | row("Hola", "Mundo") 68 | } 69 | ``` 70 | ``` 71 | Hello│World 72 | ─────┼───── 73 | Hola │Mundo 74 | ``` 75 | 76 | ### Padding 77 | 78 | ```kotlin 79 | table { 80 | cellStyle { 81 | border = true 82 | paddingLeft = 1 83 | paddingRight = 1 84 | } 85 | row("Hello", "World") 86 | row("Hola", "Mundo") 87 | } 88 | ``` 89 | ``` 90 | ┌───────┬───────┐ 91 | │ Hello │ World │ 92 | ├───────┼───────┤ 93 | │ Hola │ Mundo │ 94 | └───────┴───────┘ 95 | ``` 96 | 97 | ### Table, Row, and Cell Style 98 | 99 | ```kotlin 100 | table { 101 | cellStyle { 102 | border = true 103 | paddingLeft = 1 104 | paddingRight = 1 105 | } 106 | row { 107 | cellStyle { 108 | // These options affect only the cells contained within this row and override table options. 109 | paddingTop = 1 110 | } 111 | cell("Hello") 112 | cell("World") 113 | } 114 | row { 115 | cell("Hola") 116 | cell("Mundo") { 117 | // These options affect only this specific cell and override row and table options. 118 | border = false 119 | } 120 | } 121 | } 122 | ``` 123 | ``` 124 | ┌───────┬───────┐ 125 | │ │ │ 126 | │ Hello │ World │ 127 | ├───────┼───────┘ 128 | │ Hola │ Mundo 129 | └───────┘ 130 | ``` 131 | 132 | ### Header and Footer 133 | 134 | ```kotlin 135 | table { 136 | header { 137 | // Rows in a header always come first no matter when they're added. 138 | row("Hello", "Header") 139 | } 140 | footer { 141 | // Rows in a footer always come last no matter when they're added. 142 | row("Hello", "Footer") 143 | } 144 | row("Hello", "World") 145 | cellStyle { 146 | border = true 147 | } 148 | } 149 | ``` 150 | ``` 151 | ┌─────┬──────┐ 152 | │Hello│Header│ 153 | ├─────┼──────┤ 154 | │Hello│World │ 155 | ├─────┼──────┤ 156 | │Hello│Footer│ 157 | └─────┴──────┘ 158 | ``` 159 | 160 | ### Row and Column Span 161 | 162 | ```kotlin 163 | table { 164 | cellStyle { 165 | border = true 166 | } 167 | row { 168 | cell("Hello") { 169 | rowSpan = 2 170 | } 171 | cell("World") 172 | } 173 | // This row has only one cell because "Hello" will carry over and push it to the right. 174 | row("Mars") 175 | 176 | // This row has only one cell because it spans two columns. 177 | row { 178 | cell("Hola Mundo") { 179 | columnSpan = 2 180 | } 181 | } 182 | } 183 | ``` 184 | ``` 185 | ┌─────┬─────┐ 186 | │Hello│World│ 187 | │ ├─────┤ 188 | │ │Mars │ 189 | ├─────┴─────┤ 190 | │Hola Mundo │ 191 | └───────────┘ 192 | ``` 193 | 194 | ### Text Alignment 195 | 196 | ```kotlin 197 | table { 198 | cellStyle { 199 | border = true 200 | alignment = TopCenter 201 | } 202 | row { 203 | cell("Hello") { 204 | rowSpan = 4 205 | alignment = MiddleLeft 206 | } 207 | cell("Mercury") 208 | } 209 | row("Venus") 210 | row("Earth") 211 | row("Mars") 212 | row { 213 | cell("Hola") { 214 | rowSpan = 4 215 | alignment = MiddleLeft 216 | } 217 | cell("Jupiter") 218 | } 219 | row("Saturn") 220 | row("Uranus") 221 | row("Neptune") 222 | row("Adios", "Pluto") 223 | } 224 | ``` 225 | ``` 226 | ┌─────┬───────┐ 227 | │ │Mercury│ 228 | │ ├───────┤ 229 | │ │ Venus │ 230 | │Hello├───────┤ 231 | │ │ Earth │ 232 | │ ├───────┤ 233 | │ │ Mars │ 234 | ├─────┼───────┤ 235 | │ │Jupiter│ 236 | │ ├───────┤ 237 | │ │Saturn │ 238 | │Hola ├───────┤ 239 | │ │Uranus │ 240 | │ ├───────┤ 241 | │ │Neptune│ 242 | ├─────┼───────┤ 243 | │Adios│ Pluto │ 244 | └─────┴───────┘ 245 | ``` 246 | 247 | ### Real-world Example 248 | 249 | Here is a more advanced, real-world example from the 250 | [Diffuse](https://github.com/JakeWharton/diffuse/) tool. It features row and column spans, headers 251 | and footers, borders, table border style, padding, and text alignment. 252 | 253 | ```kotlin 254 | table { 255 | style { 256 | borderStyle = Hidden 257 | } 258 | cellStyle { 259 | alignment = MiddleRight 260 | paddingLeft = 1 261 | paddingRight = 1 262 | borderLeft = true 263 | borderRight = true 264 | } 265 | header { 266 | cellStyle { 267 | border = true 268 | alignment = BottomLeft 269 | } 270 | row { 271 | cell("APK") { 272 | rowSpan = 2 273 | } 274 | cell("compressed") { 275 | alignment = BottomCenter 276 | columnSpan = 3 277 | } 278 | cell("uncompressed") { 279 | alignment = BottomCenter 280 | columnSpan = 3 281 | } 282 | } 283 | row("old", "new", "diff", "old", "new", "diff") 284 | } 285 | body { 286 | row("dex", "664.8 KiB", "664.8 Kib", "-25 B", "1.5 MiB", "1.5 MiB", "-112 B") 287 | // "arsc", "manifest", etc… 288 | } 289 | footer { 290 | cellStyle { 291 | border = true 292 | } 293 | row("total", "1.3 MiB", "1.3 MiB", "-39 B", "2.2 MiB", "2.2 MiB", "-112 B") 294 | } 295 | } 296 | ``` 297 | ``` 298 | │ compressed │ uncompressed 299 | ├───────────┬───────────┬───────┼───────────┬───────────┬──────── 300 | APK │ old │ new │ diff │ old │ new │ diff 301 | ──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────── 302 | dex │ 664.8 KiB │ 664.8 KiB │ -25 B │ 1.5 MiB │ 1.5 MiB │ -112 B 303 | arsc │ 201.7 KiB │ 201.7 KiB │ 0 B │ 201.6 KiB │ 201.6 KiB │ 0 B 304 | manifest │ 1.4 KiB │ 1.4 KiB │ 0 B │ 4.2 KiB │ 4.2 KiB │ 0 B 305 | res │ 418.2 KiB │ 418.2 KiB │ -14 B │ 488.3 KiB │ 488.3 KiB │ 0 B 306 | asset │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B 307 | other │ 37.1 KiB │ 37.1 KiB │ 0 B │ 36.3 KiB │ 36.3 KiB │ 0 B 308 | ──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────── 309 | total │ 1.3 MiB │ 1.3 MiB │ -39 B │ 2.2 MiB │ 2.2 MiB │ -112 B 310 | ``` 311 | 312 | 313 | Download 314 | -------- 315 | 316 | ```groovy 317 | repositories { 318 | mavenCentral() 319 | } 320 | dependencies { 321 | implementation 'com.jakewharton.picnic:picnic:0.7.0' 322 | } 323 | ``` 324 | 325 |
326 | Snapshots of the development version are available in the Central Portal Snapshots repository. 327 |

328 | 329 | ```groovy 330 | repositories { 331 | maven { 332 | url 'https://central.sonatype.com/repository/maven-snapshots/' 333 | } 334 | } 335 | dependencies { 336 | implementation 'com.jakewharton.picnic:picnic:0.8.0-SNAPSHOT' 337 | } 338 | ``` 339 | 340 |

341 |
342 | 343 | 344 | 345 | License 346 | ======= 347 | 348 | Copyright 2015 Jake Wharton 349 | 350 | Licensed under the Apache License, Version 2.0 (the "License"); 351 | you may not use this file except in compliance with the License. 352 | You may obtain a copy of the License at 353 | 354 | http://www.apache.org/licenses/LICENSE-2.0 355 | 356 | Unless required by applicable law or agreed to in writing, software 357 | distributed under the License is distributed on an "AS IS" BASIS, 358 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 359 | See the License for the specific language governing permissions and 360 | limitations under the License. 361 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update the `VERSION_NAME` in `gradle.properties` to the release version. 4 | 5 | 2. Update the `CHANGELOG.md`: 6 | 1. Change the `Unreleased` header to the release version. 7 | 2. Add a link URL to ensure the header link works. 8 | 3. Add a new `Unreleased` section to the top. 9 | 10 | 3. Update the `README.md` so the "Download" section reflects the new release version and the 11 | snapshot section reflects the next "SNAPSHOT" version. 12 | 13 | 4. Commit 14 | 15 | ``` 16 | $ git commit -am "Prepare version X.Y.X" 17 | ``` 18 | 19 | 5. Publish 20 | 21 | ``` 22 | $ ./gradlew clean publish 23 | ``` 24 | 25 | If this step fails, drop the Sonatype repo, fix, commit, and publish again. 26 | 27 | 6. Tag 28 | 29 | ``` 30 | $ git tag -am "Version X.Y.Z" X.Y.Z 31 | ``` 32 | 33 | 7. Update the `VERSION_NAME` in `gradle.properties` to the next "SNAPSHOT" version. 34 | 35 | 8. Commit 36 | 37 | ``` 38 | $ git commit -am "Prepare next development version" 39 | ``` 40 | 41 | 9. Push! 42 | 43 | ``` 44 | $ git push && git push --tags 45 | ``` 46 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | buildscript { 5 | dependencies { 6 | classpath libs.kotlinPlugin 7 | classpath libs.spotlessPlugin 8 | classpath libs.dokkaPlugin 9 | classpath libs.animalSnifferPlugin 10 | classpath libs.publishPlugin 11 | } 12 | 13 | repositories { 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenCentral() 22 | } 23 | } 24 | 25 | subprojects { 26 | apply plugin: 'com.diffplug.spotless' 27 | spotless { 28 | kotlin { 29 | ktlint().editorConfigOverride([ 30 | 'ktlint_standard_filename': 'disabled', 31 | ]) 32 | } 33 | } 34 | 35 | tasks.withType(KotlinCompile).configureEach { 36 | compilerOptions { 37 | jvmTarget = JvmTarget.JVM_1_8 38 | freeCompilerArgs.addAll([ 39 | '-progressive', 40 | '-Xjvm-default=all', 41 | ]) 42 | } 43 | } 44 | 45 | tasks.withType(JavaCompile).configureEach { 46 | sourceCompatibility = JavaVersion.VERSION_1_8.toString() 47 | targetCompatibility = JavaVersion.VERSION_1_8.toString() 48 | } 49 | 50 | plugins.withId('com.vanniktech.maven.publish') { 51 | project.apply plugin: 'ru.vyarus.animalsniffer' 52 | 53 | dependencies { 54 | signature 'org.codehaus.mojo.signature:java18:1.0@signature' 55 | signature 'net.sf.androidscents.signature:android-api-level-21:5.0.1_r2@signature' 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.jakewharton.picnic 2 | VERSION_NAME=0.8.0-SNAPSHOT 3 | 4 | POM_DESCRIPTION=An API for constructing HTML-like tables which can be rendered to text 5 | 6 | POM_URL=https://github.com/JakeWharton/picnic/ 7 | POM_SCM_URL=https://github.com/JakeWharton/picnic/ 8 | POM_SCM_CONNECTION=scm:git:git://github.com/JakeWharton/picnic.git 9 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/JakeWharton/picnic.git 10 | 11 | POM_LICENCE_NAME=Apache-2.0 12 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=jakewharton 16 | POM_DEVELOPER_NAME=Jake Wharton 17 | 18 | kotlin.mpp.stability.nowarn=true 19 | 20 | SONATYPE_HOST=CENTRAL_PORTAL 21 | SONATYPE_AUTOMATIC_RELEASE=true 22 | RELEASE_SIGNING_ENABLED=true 23 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [libraries] 2 | kotlinPlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21" 3 | spotlessPlugin = 'com.diffplug.spotless:spotless-plugin-gradle:7.0.4' 4 | dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0" 5 | animalSnifferPlugin = "ru.vyarus:gradle-animalsniffer-plugin:2.0.1" 6 | publishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.32.0" 7 | 8 | crossword = "com.jakewharton.crossword:crossword:0.4.0" 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeWharton/picnic/d1bfd0c1765f62ee121fbfee43c45b0801ed8d68/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /kotlin-js-store/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ansi-colors@^4.1.3: 6 | version "4.1.3" 7 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" 8 | integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== 9 | 10 | ansi-regex@^5.0.1: 11 | version "5.0.1" 12 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 13 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 14 | 15 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 16 | version "4.3.0" 17 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 18 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 19 | dependencies: 20 | color-convert "^2.0.1" 21 | 22 | anymatch@~3.1.2: 23 | version "3.1.2" 24 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 25 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 26 | dependencies: 27 | normalize-path "^3.0.0" 28 | picomatch "^2.0.4" 29 | 30 | argparse@^2.0.1: 31 | version "2.0.1" 32 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 33 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 34 | 35 | balanced-match@^1.0.0: 36 | version "1.0.2" 37 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 38 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 39 | 40 | binary-extensions@^2.0.0: 41 | version "2.2.0" 42 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 43 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 44 | 45 | brace-expansion@^2.0.1: 46 | version "2.0.1" 47 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 48 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 49 | dependencies: 50 | balanced-match "^1.0.0" 51 | 52 | braces@~3.0.2: 53 | version "3.0.2" 54 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 55 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 56 | dependencies: 57 | fill-range "^7.0.1" 58 | 59 | browser-stdout@^1.3.1: 60 | version "1.3.1" 61 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 62 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 63 | 64 | buffer-from@^1.0.0: 65 | version "1.1.2" 66 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 67 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 68 | 69 | camelcase@^6.0.0: 70 | version "6.3.0" 71 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" 72 | integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== 73 | 74 | chalk@^4.1.0: 75 | version "4.1.2" 76 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 77 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 78 | dependencies: 79 | ansi-styles "^4.1.0" 80 | supports-color "^7.1.0" 81 | 82 | chokidar@^3.5.3: 83 | version "3.6.0" 84 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" 85 | integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== 86 | dependencies: 87 | anymatch "~3.1.2" 88 | braces "~3.0.2" 89 | glob-parent "~5.1.2" 90 | is-binary-path "~2.1.0" 91 | is-glob "~4.0.1" 92 | normalize-path "~3.0.0" 93 | readdirp "~3.6.0" 94 | optionalDependencies: 95 | fsevents "~2.3.2" 96 | 97 | cliui@^7.0.2: 98 | version "7.0.4" 99 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" 100 | integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== 101 | dependencies: 102 | string-width "^4.2.0" 103 | strip-ansi "^6.0.0" 104 | wrap-ansi "^7.0.0" 105 | 106 | color-convert@^2.0.1: 107 | version "2.0.1" 108 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 109 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 110 | dependencies: 111 | color-name "~1.1.4" 112 | 113 | color-name@~1.1.4: 114 | version "1.1.4" 115 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 116 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 117 | 118 | debug@^4.3.5: 119 | version "4.3.6" 120 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" 121 | integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== 122 | dependencies: 123 | ms "2.1.2" 124 | 125 | decamelize@^4.0.0: 126 | version "4.0.0" 127 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" 128 | integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== 129 | 130 | diff@^5.2.0: 131 | version "5.2.0" 132 | resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" 133 | integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== 134 | 135 | emoji-regex@^8.0.0: 136 | version "8.0.0" 137 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 138 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 139 | 140 | escalade@^3.1.1: 141 | version "3.1.1" 142 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 143 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 144 | 145 | escape-string-regexp@^4.0.0: 146 | version "4.0.0" 147 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 148 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 149 | 150 | fill-range@^7.0.1: 151 | version "7.0.1" 152 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 153 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 154 | dependencies: 155 | to-regex-range "^5.0.1" 156 | 157 | find-up@^5.0.0: 158 | version "5.0.0" 159 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 160 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 161 | dependencies: 162 | locate-path "^6.0.0" 163 | path-exists "^4.0.0" 164 | 165 | flat@^5.0.2: 166 | version "5.0.2" 167 | resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" 168 | integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== 169 | 170 | format-util@^1.0.5: 171 | version "1.0.5" 172 | resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" 173 | integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== 174 | 175 | fs.realpath@^1.0.0: 176 | version "1.0.0" 177 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 178 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 179 | 180 | fsevents@~2.3.2: 181 | version "2.3.2" 182 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 183 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 184 | 185 | get-caller-file@^2.0.5: 186 | version "2.0.5" 187 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 188 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 189 | 190 | glob-parent@~5.1.2: 191 | version "5.1.2" 192 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 193 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 194 | dependencies: 195 | is-glob "^4.0.1" 196 | 197 | glob@^8.1.0: 198 | version "8.1.0" 199 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" 200 | integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== 201 | dependencies: 202 | fs.realpath "^1.0.0" 203 | inflight "^1.0.4" 204 | inherits "2" 205 | minimatch "^5.0.1" 206 | once "^1.3.0" 207 | 208 | has-flag@^4.0.0: 209 | version "4.0.0" 210 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 211 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 212 | 213 | he@^1.2.0: 214 | version "1.2.0" 215 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 216 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 217 | 218 | inflight@^1.0.4: 219 | version "1.0.6" 220 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 221 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 222 | dependencies: 223 | once "^1.3.0" 224 | wrappy "1" 225 | 226 | inherits@2: 227 | version "2.0.4" 228 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 229 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 230 | 231 | is-binary-path@~2.1.0: 232 | version "2.1.0" 233 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 234 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 235 | dependencies: 236 | binary-extensions "^2.0.0" 237 | 238 | is-extglob@^2.1.1: 239 | version "2.1.1" 240 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 241 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 242 | 243 | is-fullwidth-code-point@^3.0.0: 244 | version "3.0.0" 245 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 246 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 247 | 248 | is-glob@^4.0.1, is-glob@~4.0.1: 249 | version "4.0.3" 250 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 251 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 252 | dependencies: 253 | is-extglob "^2.1.1" 254 | 255 | is-number@^7.0.0: 256 | version "7.0.0" 257 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 258 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 259 | 260 | is-plain-obj@^2.1.0: 261 | version "2.1.0" 262 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 263 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 264 | 265 | is-unicode-supported@^0.1.0: 266 | version "0.1.0" 267 | resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" 268 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== 269 | 270 | js-yaml@^4.1.0: 271 | version "4.1.0" 272 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 273 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 274 | dependencies: 275 | argparse "^2.0.1" 276 | 277 | kotlin-web-helpers@2.0.0: 278 | version "2.0.0" 279 | resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.0.0.tgz#b112096b273c1e733e0b86560998235c09a19286" 280 | integrity sha512-xkVGl60Ygn/zuLkDPx+oHj7jeLR7hCvoNF99nhwXMn8a3ApB4lLiC9pk4ol4NHPjyoCbvQctBqvzUcp8pkqyWw== 281 | dependencies: 282 | format-util "^1.0.5" 283 | 284 | locate-path@^6.0.0: 285 | version "6.0.0" 286 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 287 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 288 | dependencies: 289 | p-locate "^5.0.0" 290 | 291 | log-symbols@^4.1.0: 292 | version "4.1.0" 293 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" 294 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== 295 | dependencies: 296 | chalk "^4.1.0" 297 | is-unicode-supported "^0.1.0" 298 | 299 | minimatch@^5.0.1, minimatch@^5.1.6: 300 | version "5.1.6" 301 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" 302 | integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== 303 | dependencies: 304 | brace-expansion "^2.0.1" 305 | 306 | mocha@10.7.3: 307 | version "10.7.3" 308 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" 309 | integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== 310 | dependencies: 311 | ansi-colors "^4.1.3" 312 | browser-stdout "^1.3.1" 313 | chokidar "^3.5.3" 314 | debug "^4.3.5" 315 | diff "^5.2.0" 316 | escape-string-regexp "^4.0.0" 317 | find-up "^5.0.0" 318 | glob "^8.1.0" 319 | he "^1.2.0" 320 | js-yaml "^4.1.0" 321 | log-symbols "^4.1.0" 322 | minimatch "^5.1.6" 323 | ms "^2.1.3" 324 | serialize-javascript "^6.0.2" 325 | strip-json-comments "^3.1.1" 326 | supports-color "^8.1.1" 327 | workerpool "^6.5.1" 328 | yargs "^16.2.0" 329 | yargs-parser "^20.2.9" 330 | yargs-unparser "^2.0.0" 331 | 332 | ms@2.1.2: 333 | version "2.1.2" 334 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 335 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 336 | 337 | ms@^2.1.3: 338 | version "2.1.3" 339 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 340 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 341 | 342 | normalize-path@^3.0.0, normalize-path@~3.0.0: 343 | version "3.0.0" 344 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 345 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 346 | 347 | once@^1.3.0: 348 | version "1.4.0" 349 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 350 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 351 | dependencies: 352 | wrappy "1" 353 | 354 | p-limit@^3.0.2: 355 | version "3.1.0" 356 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 357 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 358 | dependencies: 359 | yocto-queue "^0.1.0" 360 | 361 | p-locate@^5.0.0: 362 | version "5.0.0" 363 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 364 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 365 | dependencies: 366 | p-limit "^3.0.2" 367 | 368 | path-exists@^4.0.0: 369 | version "4.0.0" 370 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 371 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 372 | 373 | picomatch@^2.0.4, picomatch@^2.2.1: 374 | version "2.3.1" 375 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 376 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 377 | 378 | randombytes@^2.1.0: 379 | version "2.1.0" 380 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 381 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 382 | dependencies: 383 | safe-buffer "^5.1.0" 384 | 385 | readdirp@~3.6.0: 386 | version "3.6.0" 387 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 388 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 389 | dependencies: 390 | picomatch "^2.2.1" 391 | 392 | require-directory@^2.1.1: 393 | version "2.1.1" 394 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 395 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 396 | 397 | safe-buffer@^5.1.0: 398 | version "5.2.1" 399 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 400 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 401 | 402 | serialize-javascript@^6.0.2: 403 | version "6.0.2" 404 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" 405 | integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== 406 | dependencies: 407 | randombytes "^2.1.0" 408 | 409 | source-map-support@0.5.21: 410 | version "0.5.21" 411 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 412 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 413 | dependencies: 414 | buffer-from "^1.0.0" 415 | source-map "^0.6.0" 416 | 417 | source-map@^0.6.0: 418 | version "0.6.1" 419 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 420 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 421 | 422 | string-width@^4.1.0, string-width@^4.2.0: 423 | version "4.2.3" 424 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 425 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 426 | dependencies: 427 | emoji-regex "^8.0.0" 428 | is-fullwidth-code-point "^3.0.0" 429 | strip-ansi "^6.0.1" 430 | 431 | strip-ansi@^6.0.0, strip-ansi@^6.0.1: 432 | version "6.0.1" 433 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 434 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 435 | dependencies: 436 | ansi-regex "^5.0.1" 437 | 438 | strip-json-comments@^3.1.1: 439 | version "3.1.1" 440 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 441 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 442 | 443 | supports-color@^7.1.0: 444 | version "7.2.0" 445 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 446 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 447 | dependencies: 448 | has-flag "^4.0.0" 449 | 450 | supports-color@^8.1.1: 451 | version "8.1.1" 452 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" 453 | integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== 454 | dependencies: 455 | has-flag "^4.0.0" 456 | 457 | to-regex-range@^5.0.1: 458 | version "5.0.1" 459 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 460 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 461 | dependencies: 462 | is-number "^7.0.0" 463 | 464 | typescript@5.5.4: 465 | version "5.5.4" 466 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" 467 | integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== 468 | 469 | workerpool@^6.5.1: 470 | version "6.5.1" 471 | resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" 472 | integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== 473 | 474 | wrap-ansi@^7.0.0: 475 | version "7.0.0" 476 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 477 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 478 | dependencies: 479 | ansi-styles "^4.0.0" 480 | string-width "^4.1.0" 481 | strip-ansi "^6.0.0" 482 | 483 | wrappy@1: 484 | version "1.0.2" 485 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 486 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 487 | 488 | y18n@^5.0.5: 489 | version "5.0.8" 490 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" 491 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 492 | 493 | yargs-parser@^20.2.2, yargs-parser@^20.2.9: 494 | version "20.2.9" 495 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" 496 | integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== 497 | 498 | yargs-unparser@^2.0.0: 499 | version "2.0.0" 500 | resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" 501 | integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== 502 | dependencies: 503 | camelcase "^6.0.0" 504 | decamelize "^4.0.0" 505 | flat "^5.0.2" 506 | is-plain-obj "^2.1.0" 507 | 508 | yargs@^16.2.0: 509 | version "16.2.0" 510 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" 511 | integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== 512 | dependencies: 513 | cliui "^7.0.2" 514 | escalade "^3.1.1" 515 | get-caller-file "^2.0.5" 516 | require-directory "^2.1.1" 517 | string-width "^4.2.0" 518 | y18n "^5.0.5" 519 | yargs-parser "^20.2.2" 520 | 521 | yocto-queue@^0.1.0: 522 | version "0.1.0" 523 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 524 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 525 | -------------------------------------------------------------------------------- /picnic/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.jetbrains.kotlin.multiplatform' 2 | apply plugin: 'com.vanniktech.maven.publish' 3 | 4 | kotlin { 5 | androidNativeArm32() 6 | androidNativeArm64() 7 | androidNativeX86() 8 | androidNativeX64() 9 | iosArm64() 10 | iosX64() 11 | iosSimulatorArm64() 12 | js().nodejs() 13 | jvm() 14 | linuxArm64() 15 | linuxX64() 16 | macosX64() 17 | macosArm64() 18 | mingwX64() 19 | tvosArm64() 20 | tvosX64() 21 | tvosSimulatorArm64() 22 | wasmJs().nodejs() 23 | wasmWasi().nodejs() 24 | watchosArm32() 25 | watchosArm64() 26 | watchosDeviceArm64() 27 | watchosX64() 28 | watchosSimulatorArm64() 29 | 30 | sourceSets { 31 | commonMain { 32 | dependencies { 33 | implementation libs.crossword 34 | } 35 | } 36 | commonTest { 37 | dependencies { 38 | implementation 'org.jetbrains.kotlin:kotlin-test' 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /picnic/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=picnic 2 | POM_NAME=Picnic 3 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/IntCounts.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | internal class IntCounts(capacity: Int = 10) { 4 | private var data = IntArray(capacity) 5 | 6 | var size = 0 7 | private set 8 | 9 | operator fun get(index: Int) = if (index >= size) 0 else data[index] 10 | 11 | operator fun set(index: Int, value: Int) { 12 | val newSize = index + 1 13 | if (newSize > data.size) { 14 | data = data.copyOf(data.size * 2) 15 | } 16 | data[index] = value 17 | size = size.coerceAtLeast(newSize) 18 | } 19 | 20 | override fun toString() = buildString { 21 | append('[') 22 | repeat(size) { 23 | if (it > 0) { 24 | append(", ") 25 | } 26 | append(data[it]) 27 | } 28 | append(']') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/dsl.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("-DslKt") 2 | 3 | package com.jakewharton.picnic 4 | 5 | import kotlin.DeprecationLevel.ERROR 6 | import kotlin.jvm.JvmName 7 | import kotlin.jvm.JvmSynthetic 8 | 9 | @DslMarker 10 | private annotation class PicnicDsl 11 | 12 | fun table(content: TableDsl.() -> Unit) = TableDslImpl().apply(content).create() 13 | 14 | @PicnicDsl 15 | interface TableDsl : TableSectionDsl { 16 | fun header(content: TableSectionDsl.() -> Unit) 17 | fun body(content: TableSectionDsl.() -> Unit) 18 | fun footer(content: TableSectionDsl.() -> Unit) 19 | fun style(content: TableStyleDsl.() -> Unit) 20 | } 21 | 22 | @PicnicDsl 23 | interface TableStyleDsl { 24 | var border: Boolean? 25 | var borderStyle: BorderStyle? 26 | } 27 | 28 | @PicnicDsl 29 | interface TableSectionDsl { 30 | fun row(vararg cells: Any?) { 31 | row { 32 | cells.forEach { cell(it) } 33 | } 34 | } 35 | 36 | fun row(content: RowDsl.() -> Unit) 37 | 38 | fun cellStyle(content: CellStyleDsl.() -> Unit) 39 | } 40 | 41 | @PicnicDsl 42 | interface RowDsl { 43 | fun cell(content: Any?, style: CellDsl.() -> Unit = {}) 44 | 45 | fun cells(vararg content: Any?, style: CellDsl.() -> Unit = {}) { 46 | content.forEach { cell(it, style) } 47 | } 48 | 49 | fun cellStyle(content: CellStyleDsl.() -> Unit) 50 | } 51 | 52 | @PicnicDsl 53 | interface CellDsl : CellStyleDsl { 54 | var columnSpan: Int 55 | var rowSpan: Int 56 | } 57 | 58 | @PicnicDsl 59 | interface CellStyleDsl { 60 | var paddingLeft: Int? 61 | var paddingRight: Int? 62 | var paddingTop: Int? 63 | var paddingBottom: Int? 64 | 65 | var borderLeft: Boolean? 66 | var borderRight: Boolean? 67 | var borderTop: Boolean? 68 | var borderBottom: Boolean? 69 | 70 | var alignment: TextAlignment? 71 | 72 | var border: Boolean 73 | @JvmSynthetic 74 | @Deprecated("Use individual getters", level = ERROR) 75 | get() = throw UnsupportedOperationException() 76 | set(value) { 77 | borderLeft = value 78 | borderRight = value 79 | borderTop = value 80 | borderBottom = value 81 | } 82 | 83 | var padding: Int 84 | @JvmSynthetic 85 | @Deprecated("Use individual getters", level = ERROR) 86 | get() = throw UnsupportedOperationException() 87 | set(value) { 88 | paddingLeft = value 89 | paddingRight = value 90 | paddingTop = value 91 | paddingBottom = value 92 | } 93 | } 94 | 95 | private class TableDslImpl : TableDsl { 96 | private val headerImpl = TableSectionDslImpl() 97 | private val bodyImpl = TableSectionDslImpl() 98 | private val footerImpl = TableSectionDslImpl() 99 | private val cellStyleImpl = CellStyleDslImpl() 100 | private val tableStyleImpl = TableStyleDslImpl() 101 | 102 | override fun header(content: TableSectionDsl.() -> Unit) { 103 | headerImpl.apply(content) 104 | } 105 | 106 | override fun body(content: TableSectionDsl.() -> Unit) { 107 | bodyImpl.apply(content) 108 | } 109 | 110 | override fun footer(content: TableSectionDsl.() -> Unit) { 111 | footerImpl.apply(content) 112 | } 113 | 114 | override fun row(content: RowDsl.() -> Unit) { 115 | bodyImpl.row(content) 116 | } 117 | 118 | override fun cellStyle(content: CellStyleDsl.() -> Unit) { 119 | cellStyleImpl.apply(content) 120 | } 121 | 122 | override fun style(content: TableStyleDsl.() -> Unit) { 123 | tableStyleImpl.apply(content) 124 | } 125 | 126 | fun create() = Table { 127 | header = headerImpl.createOrNull() 128 | body = bodyImpl.create() 129 | footer = footerImpl.createOrNull() 130 | cellStyle = cellStyleImpl.createOrNull() 131 | tableStyle = tableStyleImpl.createOrNull() 132 | } 133 | } 134 | 135 | private class TableSectionDslImpl : TableSectionDsl { 136 | private val builder = TableSection.Builder() 137 | private val cellStyleImpl = CellStyleDslImpl() 138 | 139 | override fun row(content: RowDsl.() -> Unit) { 140 | builder.addRow(RowDslImpl().apply(content).create()) 141 | } 142 | 143 | override fun cellStyle(content: CellStyleDsl.() -> Unit) { 144 | cellStyleImpl.apply(content) 145 | } 146 | 147 | fun createOrNull() = if (builder.rows.isEmpty()) null else create() 148 | fun create() = builder.setCellStyle(cellStyleImpl.createOrNull()).build() 149 | } 150 | 151 | private class RowDslImpl : RowDsl { 152 | private val builder = Row.Builder() 153 | private val cellStyleImpl = CellStyleDslImpl() 154 | 155 | override fun cell(content: Any?, style: CellDsl.() -> Unit) { 156 | builder.addCell(CellDslImpl(content).apply(style).create()) 157 | } 158 | 159 | override fun cellStyle(content: CellStyleDsl.() -> Unit) { 160 | cellStyleImpl.apply(content) 161 | } 162 | 163 | fun create() = builder.setCellStyle(cellStyleImpl.createOrNull()).build() 164 | } 165 | 166 | private class CellDslImpl private constructor( 167 | private val content: Any?, 168 | private val cellStyleImpl: CellStyleDslImpl, 169 | ) : CellDsl, CellStyleDsl by cellStyleImpl { 170 | 171 | constructor(content: Any?) : this(content, CellStyleDslImpl()) 172 | 173 | override var columnSpan: Int = 1 174 | override var rowSpan: Int = 1 175 | 176 | fun create() = Cell(content?.toString() ?: "") { 177 | columnSpan = this@CellDslImpl.columnSpan 178 | rowSpan = this@CellDslImpl.rowSpan 179 | style = cellStyleImpl.createOrNull() 180 | } 181 | } 182 | 183 | private class CellStyleDslImpl : CellStyleDsl { 184 | override var paddingLeft: Int? = null 185 | override var paddingRight: Int? = null 186 | override var paddingTop: Int? = null 187 | override var paddingBottom: Int? = null 188 | override var borderLeft: Boolean? = null 189 | override var borderRight: Boolean? = null 190 | override var borderTop: Boolean? = null 191 | override var borderBottom: Boolean? = null 192 | override var alignment: TextAlignment? = null 193 | 194 | fun createOrNull(): CellStyle? { 195 | if (paddingLeft != null || 196 | paddingRight != null || 197 | paddingTop != null || 198 | paddingBottom != null || 199 | borderLeft != null || 200 | borderRight != null || 201 | borderTop != null || 202 | borderBottom != null || 203 | alignment != null 204 | ) { 205 | return CellStyle { 206 | paddingLeft = this@CellStyleDslImpl.paddingLeft 207 | paddingRight = this@CellStyleDslImpl.paddingRight 208 | paddingTop = this@CellStyleDslImpl.paddingTop 209 | paddingBottom = this@CellStyleDslImpl.paddingBottom 210 | borderLeft = this@CellStyleDslImpl.borderLeft 211 | borderRight = this@CellStyleDslImpl.borderRight 212 | borderTop = this@CellStyleDslImpl.borderTop 213 | borderBottom = this@CellStyleDslImpl.borderBottom 214 | alignment = this@CellStyleDslImpl.alignment 215 | } 216 | } 217 | return null 218 | } 219 | } 220 | 221 | private class TableStyleDslImpl : TableStyleDsl { 222 | override var border: Boolean? = null 223 | override var borderStyle: BorderStyle? = null 224 | 225 | fun createOrNull(): TableStyle? { 226 | if (border != null || borderStyle != null) { 227 | return TableStyle { 228 | border = this@TableStyleDslImpl.border 229 | borderStyle = this@TableStyleDslImpl.borderStyle 230 | } 231 | } 232 | return null 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/model.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import kotlin.jvm.JvmSynthetic 4 | 5 | class Table private constructor( 6 | val header: TableSection?, 7 | val body: TableSection, 8 | val footer: TableSection?, 9 | val cellStyle: CellStyle?, 10 | val tableStyle: TableStyle?, 11 | ) { 12 | override fun toString() = renderText() 13 | override fun hashCode() = hash(header, body, footer, cellStyle, tableStyle) 14 | override fun equals(other: Any?) = other is Table && 15 | header == other.header && 16 | body == other.body && 17 | footer == other.footer && 18 | cellStyle == other.cellStyle && 19 | tableStyle == other.tableStyle 20 | 21 | val rowCount: Int = (header?.rows?.size ?: 0) + body.rows.size + (footer?.rows?.size ?: 0) 22 | val columnCount: Int 23 | val positionedCells: List 24 | 25 | private val cellTable: List> 26 | 27 | init { 28 | val rowSpanCarries = IntCounts() 29 | val positionedCells = mutableListOf() 30 | val cellTable = mutableListOf>() 31 | var rowIndex = 0 32 | listOfNotNull(header, body, footer).forEach { section -> 33 | val sectionStyle = cellStyle + section.cellStyle 34 | 35 | section.rows.forEach { row -> 36 | val rowStyle = sectionStyle + row.cellStyle 37 | 38 | val cellRow = mutableListOf() 39 | cellTable += cellRow 40 | 41 | var columnIndex = 0 42 | row.cells.forEachIndexed { rawColumnIndex, cell -> 43 | // Check for any previous rows' cells whose >1 rowSpan carries them into this row. 44 | // When found, add them to the current row, pushing remaining cells to the right. 45 | while (columnIndex < rowSpanCarries.size && rowSpanCarries[columnIndex] > 0) { 46 | cellRow += cellTable[rowIndex - 1][columnIndex] 47 | rowSpanCarries[columnIndex]-- 48 | columnIndex++ 49 | } 50 | 51 | val canonicalStyle = rowStyle + cell.style 52 | val positionedCell = PositionedCell(rowIndex, columnIndex, cell, canonicalStyle) 53 | positionedCells += positionedCell 54 | 55 | val rowSpan = cell.rowSpan 56 | require(rowIndex + rowSpan <= rowCount) { 57 | "Cell $rawColumnIndex in row $rowIndex has rowSpan=$rowSpan but table rowCount=$rowCount" 58 | } 59 | 60 | val rowSpanCarry = rowSpan - 1 61 | repeat(cell.columnSpan) { 62 | cellRow += positionedCell 63 | rowSpanCarries[columnIndex] = rowSpanCarry 64 | columnIndex++ 65 | } 66 | } 67 | 68 | // Check for any previous rows' cells whose >1 rowSpan carries them into this row. 69 | // When found, add them to the current row, filling any gaps with null. 70 | while (columnIndex < rowSpanCarries.size) { 71 | if (rowSpanCarries[columnIndex] > 0) { 72 | cellRow += cellTable[rowIndex - 1][columnIndex] 73 | rowSpanCarries[columnIndex]-- 74 | } else { 75 | cellRow.add(null) 76 | } 77 | columnIndex++ 78 | } 79 | 80 | rowIndex++ 81 | } 82 | } 83 | 84 | columnCount = rowSpanCarries.size 85 | this.positionedCells = positionedCells 86 | this.cellTable = cellTable 87 | } 88 | 89 | fun getOrNull(row: Int, column: Int) = cellTable.getOrNull(row)?.getOrNull(column) 90 | 91 | operator fun get(row: Int, column: Int) = requireNotNull(cellTable[row][column]) { 92 | "Cell was null" 93 | } 94 | 95 | class PositionedCell( 96 | val rowIndex: Int, 97 | val columnIndex: Int, 98 | val cell: Cell, 99 | val canonicalStyle: CellStyle?, 100 | ) { 101 | override fun hashCode() = hash(rowIndex, columnIndex, cell, canonicalStyle) 102 | override fun equals(other: Any?) = other is PositionedCell && 103 | rowIndex == other.rowIndex && 104 | columnIndex == other.columnIndex && 105 | cell == other.cell && 106 | canonicalStyle == other.canonicalStyle 107 | 108 | override fun toString() = 109 | "PositionedCell(rowIndex=$rowIndex, colIndex=$columnIndex, cell=$cell, " + 110 | "canonicalStyle=$canonicalStyle)" 111 | } 112 | 113 | class Builder { 114 | @set:JvmSynthetic // Hide 'void' setter from Java. 115 | var header: TableSection? = null 116 | 117 | fun setHeader(header: TableSection?) = apply { 118 | this.header = header 119 | } 120 | 121 | @set:JvmSynthetic // Hide 'void' setter from Java. 122 | var body: TableSection? = null 123 | 124 | fun setBody(body: TableSection?) = apply { 125 | this.body = body 126 | } 127 | 128 | @set:JvmSynthetic // Hide 'void' setter from Java. 129 | var footer: TableSection? = null 130 | 131 | fun setFooter(footer: TableSection?) = apply { 132 | this.footer = footer 133 | } 134 | 135 | @set:JvmSynthetic // Hide 'void' setter from Java. 136 | var cellStyle: CellStyle? = null 137 | 138 | fun setCellStyle(cellStyle: CellStyle?) = apply { 139 | this.cellStyle = cellStyle 140 | } 141 | 142 | @set:JvmSynthetic // Hide 'void' setter from Java. 143 | var tableStyle: TableStyle? = null 144 | 145 | fun setTableStyle(tableStyle: TableStyle?) = apply { 146 | this.tableStyle = tableStyle 147 | } 148 | 149 | fun build() = Table( 150 | header, 151 | checkNotNull(body) { "Body section is required" }, 152 | footer, 153 | cellStyle, 154 | tableStyle, 155 | ) 156 | } 157 | } 158 | 159 | @JvmSynthetic // Hide from Java callers who should use Builder. 160 | fun Table(initializer: Table.Builder.() -> Unit): Table { 161 | return Table.Builder().apply(initializer).build() 162 | } 163 | 164 | class TableStyle private constructor( 165 | val border: Boolean?, 166 | val borderStyle: BorderStyle?, 167 | ) { 168 | override fun toString() = "TableStyle(border=$border, borderStyle=$borderStyle)" 169 | override fun hashCode() = border.hashCode() * 37 + borderStyle.hashCode() 170 | override fun equals(other: Any?) = other is TableStyle && 171 | border == other.border && 172 | borderStyle == other.borderStyle 173 | 174 | class Builder { 175 | @set:JvmSynthetic // Hide 'void' setter from Java. 176 | var border: Boolean? = null 177 | 178 | fun setBorder(border: Boolean?) = apply { 179 | this.border = border 180 | } 181 | 182 | @set:JvmSynthetic // Hide 'void' setter from Java. 183 | var borderStyle: BorderStyle? = null 184 | 185 | fun setBorderStyle(borderStyle: BorderStyle?) = apply { 186 | this.borderStyle = borderStyle 187 | } 188 | 189 | fun build() = TableStyle(border, borderStyle) 190 | } 191 | } 192 | 193 | @JvmSynthetic // Hide from Java callers who should use Builder. 194 | fun TableStyle(initializer: TableStyle.Builder.() -> Unit): TableStyle { 195 | return TableStyle.Builder().apply(initializer).build() 196 | } 197 | 198 | enum class BorderStyle { 199 | Hidden, Solid 200 | } 201 | 202 | class TableSection private constructor( 203 | val rows: List, 204 | val cellStyle: CellStyle?, 205 | ) { 206 | override fun toString() = "TableSection(rows=$rows, cellStyle=$cellStyle)" 207 | override fun hashCode() = hash(rows, cellStyle) 208 | override fun equals(other: Any?) = other is TableSection && 209 | rows == other.rows && 210 | cellStyle == other.cellStyle 211 | 212 | class Builder { 213 | @set:JvmSynthetic // Hide 'void' setter from Java. 214 | var rows: MutableList = mutableListOf() 215 | 216 | fun setRows(rows: List) = apply { 217 | this.rows = rows.toMutableList() 218 | } 219 | 220 | fun addRow(row: Row) = apply { 221 | this.rows.add(row) 222 | } 223 | 224 | fun addRow(vararg cells: Cell) = addRow(Row { this.cells.addAll(cells) }) 225 | 226 | fun addRow(vararg cells: String) = addRow(Row { this.cells.addAll(cells.map { Cell(it) }) }) 227 | 228 | @set:JvmSynthetic // Hide 'void' setter from Java. 229 | var cellStyle: CellStyle? = null 230 | 231 | fun setCellStyle(cellStyle: CellStyle?) = apply { 232 | this.cellStyle = cellStyle 233 | } 234 | 235 | fun build() = TableSection(rows.toList(), cellStyle) 236 | } 237 | } 238 | 239 | @JvmSynthetic // Hide from Java callers who should use Builder. 240 | fun TableSection(initializer: TableSection.Builder.() -> Unit): TableSection { 241 | return TableSection.Builder().apply(initializer).build() 242 | } 243 | 244 | class Row private constructor( 245 | val cells: List, 246 | val cellStyle: CellStyle?, 247 | ) { 248 | override fun toString() = "Row(cells=$cells, cellStyle=$cellStyle)" 249 | override fun hashCode() = hash(cells, cellStyle) 250 | override fun equals(other: Any?) = other is Row && 251 | cells == other.cells && 252 | cellStyle == other.cellStyle 253 | 254 | class Builder { 255 | @set:JvmSynthetic // Hide 'void' setter from Java. 256 | var cells: MutableList = mutableListOf() 257 | 258 | fun setCells(cells: List) = apply { 259 | this.cells = cells.toMutableList() 260 | } 261 | 262 | fun addCell(cell: Cell) = apply { 263 | cells.add(cell) 264 | } 265 | 266 | fun addCell(cell: String) = apply { 267 | cells.add(Cell(cell)) 268 | } 269 | 270 | @set:JvmSynthetic // Hide 'void' setter from Java. 271 | var cellStyle: CellStyle? = null 272 | 273 | fun setCellStyle(cellStyle: CellStyle?) = apply { 274 | this.cellStyle = cellStyle 275 | } 276 | 277 | fun build() = Row(cells.toList(), cellStyle) 278 | } 279 | } 280 | 281 | @JvmSynthetic // Hide from Java callers who should use Builder. 282 | fun Row(initializer: Row.Builder.() -> Unit): Row { 283 | return Row.Builder().apply(initializer).build() 284 | } 285 | 286 | class Cell private constructor( 287 | val content: String, 288 | val columnSpan: Int, 289 | val rowSpan: Int, 290 | val style: CellStyle?, 291 | ) { 292 | override fun toString() = 293 | "Cell(content=$content, columnSpan=$columnSpan, rowSpan=$rowSpan, style=$style)" 294 | 295 | override fun hashCode() = hash(content, columnSpan, rowSpan, style) 296 | override fun equals(other: Any?) = other is Cell && 297 | content == other.content && 298 | columnSpan == other.columnSpan && 299 | rowSpan == other.rowSpan && 300 | style == other.style 301 | 302 | class Builder(val content: Any?) { 303 | @set:JvmSynthetic // Hide 'void' setter from Java. 304 | var columnSpan: Int = 1 305 | 306 | fun setColumnSpan(columnSpan: Int) = apply { 307 | this.columnSpan = columnSpan 308 | } 309 | 310 | @set:JvmSynthetic // Hide 'void' setter from Java. 311 | var rowSpan: Int = 1 312 | 313 | fun setRowSpan(rowSpan: Int) = apply { 314 | this.rowSpan = rowSpan 315 | } 316 | 317 | @set:JvmSynthetic // Hide 'void' setter from Java. 318 | var style: CellStyle? = null 319 | 320 | fun setStyle(style: CellStyle?) = apply { 321 | this.style = style 322 | } 323 | 324 | fun build() = Cell(content.toString(), columnSpan, rowSpan, style) 325 | } 326 | } 327 | 328 | @JvmSynthetic // Hide from Java callers who should use Builder. 329 | fun Cell(content: Any?, initializer: Cell.Builder.() -> Unit = {}): Cell { 330 | return Cell.Builder(content).apply(initializer).build() 331 | } 332 | 333 | class CellStyle private constructor( 334 | val paddingLeft: Int?, 335 | val paddingRight: Int?, 336 | val paddingTop: Int?, 337 | val paddingBottom: Int?, 338 | val borderLeft: Boolean?, 339 | val borderRight: Boolean?, 340 | val borderTop: Boolean?, 341 | val borderBottom: Boolean?, 342 | val alignment: TextAlignment?, 343 | ) { 344 | override fun toString() = 345 | "CellStyle(padding(l=$paddingLeft,r=$paddingRight,t=$paddingTop,b=$paddingBottom), " + 346 | "border(l=$borderLeft,r=$borderRight,t=$borderTop,b=$borderBottom), alignment=$alignment)" 347 | 348 | override fun hashCode() = hash( 349 | paddingLeft, paddingRight, paddingTop, paddingBottom, 350 | borderLeft, borderRight, borderTop, borderBottom, 351 | alignment, 352 | ) 353 | 354 | override fun equals(other: Any?) = other is CellStyle && 355 | paddingLeft == other.paddingLeft && 356 | paddingRight == other.paddingRight && 357 | paddingTop == other.paddingTop && 358 | paddingBottom == other.paddingBottom && 359 | borderLeft == other.borderLeft && 360 | borderRight == other.borderRight && 361 | borderTop == other.borderTop && 362 | borderBottom == other.borderBottom && 363 | alignment == other.alignment 364 | 365 | class Builder { 366 | @set:JvmSynthetic // Hide 'void' setter from Java. 367 | var paddingLeft: Int? = null 368 | 369 | fun setPaddingLeft(paddingLeft: Int?) = apply { 370 | this.paddingLeft = paddingLeft 371 | } 372 | 373 | @set:JvmSynthetic // Hide 'void' setter from Java. 374 | var paddingRight: Int? = null 375 | 376 | fun setPaddingRight(paddingRight: Int?) = apply { 377 | this.paddingRight = paddingRight 378 | } 379 | 380 | @set:JvmSynthetic // Hide 'void' setter from Java. 381 | var paddingTop: Int? = null 382 | 383 | fun setPaddingTop(paddingTop: Int?) = apply { 384 | this.paddingTop = paddingTop 385 | } 386 | 387 | @set:JvmSynthetic // Hide 'void' setter from Java. 388 | var paddingBottom: Int? = null 389 | 390 | fun setPaddingBottom(paddingBottom: Int?) = apply { 391 | this.paddingBottom = paddingBottom 392 | } 393 | 394 | fun setPadding(padding: Int?) = apply { 395 | paddingLeft = padding 396 | paddingRight = padding 397 | paddingTop = padding 398 | paddingBottom = padding 399 | } 400 | 401 | @set:JvmSynthetic // Hide 'void' setter from Java. 402 | var borderLeft: Boolean? = null 403 | 404 | fun setBorderLeft(borderLeft: Boolean?) = apply { 405 | this.borderLeft = borderLeft 406 | } 407 | 408 | @set:JvmSynthetic // Hide 'void' setter from Java. 409 | var borderRight: Boolean? = null 410 | 411 | fun setBorderRight(borderRight: Boolean?) = apply { 412 | this.borderRight = borderRight 413 | } 414 | 415 | @set:JvmSynthetic // Hide 'void' setter from Java. 416 | var borderTop: Boolean? = null 417 | 418 | fun setBorderTop(borderTop: Boolean?) = apply { 419 | this.borderTop = borderTop 420 | } 421 | 422 | @set:JvmSynthetic // Hide 'void' setter from Java. 423 | var borderBottom: Boolean? = null 424 | 425 | fun setBorderBottom(borderBottom: Boolean?) = apply { 426 | this.borderBottom = borderBottom 427 | } 428 | 429 | fun setBorder(border: Boolean?) = apply { 430 | borderLeft = border 431 | borderRight = border 432 | borderTop = border 433 | borderBottom = border 434 | } 435 | 436 | @set:JvmSynthetic // Hide 'void' setter from Java. 437 | var alignment: TextAlignment? = null 438 | 439 | fun setAlignment(alignment: TextAlignment?) = apply { 440 | this.alignment = alignment 441 | } 442 | 443 | fun build() = CellStyle( 444 | paddingLeft, paddingRight, paddingTop, paddingBottom, 445 | borderLeft, borderRight, borderTop, borderBottom, 446 | alignment, 447 | ) 448 | } 449 | } 450 | 451 | @JvmSynthetic // Hide from Java callers who should use Builder. 452 | fun CellStyle(initializer: CellStyle.Builder.() -> Unit): CellStyle { 453 | return CellStyle.Builder().apply(initializer).build() 454 | } 455 | 456 | private operator fun CellStyle?.plus(override: CellStyle?): CellStyle? { 457 | if (this == null) { 458 | return override 459 | } 460 | if (override == null) { 461 | return this 462 | } 463 | return CellStyle { 464 | paddingLeft = override.paddingLeft ?: this@plus.paddingLeft 465 | paddingRight = override.paddingRight ?: this@plus.paddingRight 466 | paddingTop = override.paddingTop ?: this@plus.paddingTop 467 | paddingBottom = override.paddingBottom ?: this@plus.paddingBottom 468 | borderLeft = override.borderLeft ?: this@plus.borderLeft 469 | borderRight = override.borderRight ?: this@plus.borderRight 470 | borderTop = override.borderTop ?: this@plus.borderTop 471 | borderBottom = override.borderBottom ?: this@plus.borderBottom 472 | alignment = override.alignment ?: this@plus.alignment 473 | } 474 | } 475 | 476 | enum class TextAlignment { 477 | TopLeft, TopCenter, TopRight, 478 | MiddleLeft, MiddleCenter, MiddleRight, 479 | BottomLeft, BottomCenter, BottomRight 480 | } 481 | 482 | private fun hash(vararg args: Any?): Int = args.contentHashCode() 483 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/textBorder.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import kotlin.jvm.JvmField 4 | 5 | class TextBorder(private val characters: String) { 6 | init { 7 | require(characters.length == 16) { "Border string must contain exactly 16 characters" } 8 | } 9 | 10 | val empty get() = characters[0] 11 | val down get() = characters[1] 12 | val up get() = characters[2] 13 | val vertical get() = characters[3] 14 | val right get() = characters[4] 15 | val downAndRight get() = characters[5] 16 | val upAndRight get() = characters[6] 17 | val verticalAndRight get() = characters[7] 18 | val left get() = characters[8] 19 | val downAndLeft get() = characters[9] 20 | val upAndLeft get() = characters[10] 21 | val verticalAndLeft get() = characters[11] 22 | val horizontal get() = characters[12] 23 | val downAndHorizontal get() = characters[13] 24 | val upAndHorizontal get() = characters[14] 25 | val verticalAndHorizontal get() = characters[15] 26 | 27 | fun get( 28 | down: Boolean = false, 29 | up: Boolean = false, 30 | right: Boolean = false, 31 | left: Boolean = false, 32 | ): Char { 33 | return characters[ 34 | (if (down) 1 else 0) or 35 | (if (up) 2 else 0) or 36 | (if (right) 4 else 0) or 37 | (if (left) 8 else 0) 38 | ] 39 | } 40 | 41 | companion object { 42 | @JvmField val DEFAULT = TextBorder(" ╷╵│╶┌└├╴┐┘┤─┬┴┼") 43 | @JvmField val ROUNDED = TextBorder(" ╷╵│╶╭╰├╴╮╯┤─┬┴┼") 44 | @JvmField val ASCII = TextBorder(" | +++ +++-+++") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/textLayout.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import com.jakewharton.crossword.TextCanvas 4 | import com.jakewharton.crossword.visualWidth 5 | import com.jakewharton.picnic.Table.PositionedCell 6 | import com.jakewharton.picnic.TextAlignment.BottomCenter 7 | import com.jakewharton.picnic.TextAlignment.BottomLeft 8 | import com.jakewharton.picnic.TextAlignment.BottomRight 9 | import com.jakewharton.picnic.TextAlignment.MiddleCenter 10 | import com.jakewharton.picnic.TextAlignment.MiddleLeft 11 | import com.jakewharton.picnic.TextAlignment.MiddleRight 12 | import com.jakewharton.picnic.TextAlignment.TopCenter 13 | import com.jakewharton.picnic.TextAlignment.TopLeft 14 | import com.jakewharton.picnic.TextAlignment.TopRight 15 | 16 | interface TextLayout { 17 | /** 18 | * The width in columns that this cell will occupy. 19 | * 20 | * Consider that multi-character codepoints and emoji occupy a single column. Non-printable 21 | * characters and ANSI color escape sequences occupy zero. 22 | */ 23 | fun measureWidth(): Int 24 | 25 | fun measureHeight(): Int 26 | 27 | fun draw(canvas: TextCanvas) 28 | } 29 | 30 | internal class SimpleLayout(private val cell: PositionedCell) : TextLayout { 31 | private val leftPadding = cell.canonicalStyle?.paddingLeft ?: 0 32 | private val topPadding = cell.canonicalStyle?.paddingTop ?: 0 33 | 34 | override fun measureWidth(): Int { 35 | return leftPadding + 36 | (cell.canonicalStyle?.paddingRight ?: 0) + 37 | cell.cell.content.split('\n').maxOf { it.visualWidth } 38 | } 39 | 40 | override fun measureHeight(): Int { 41 | return 1 + 42 | topPadding + 43 | (cell.canonicalStyle?.paddingBottom ?: 0) + 44 | cell.cell.content.count { it == '\n' } 45 | } 46 | 47 | override fun draw(canvas: TextCanvas) { 48 | val height = measureHeight() 49 | val alignment = cell.canonicalStyle?.alignment ?: TopLeft 50 | val top = when (alignment) { 51 | TopLeft, TopCenter, TopRight -> topPadding 52 | MiddleLeft, MiddleCenter, MiddleRight -> ((canvas.height - height) / 2) + topPadding 53 | BottomLeft, BottomCenter, BottomRight -> canvas.height - height + topPadding 54 | } 55 | 56 | cell.cell.content.split('\n').forEachIndexed { index, line -> 57 | val lineWidth = leftPadding + 58 | (cell.canonicalStyle?.paddingRight ?: 0) + 59 | line.visualWidth 60 | val left = when (alignment) { 61 | TopLeft, MiddleLeft, BottomLeft -> leftPadding 62 | TopCenter, MiddleCenter, BottomCenter -> ((canvas.width - lineWidth) / 2) + leftPadding 63 | TopRight, MiddleRight, BottomRight -> canvas.width - lineWidth + leftPadding 64 | } 65 | canvas.write(top + index, left, line) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /picnic/src/commonMain/kotlin/com/jakewharton/picnic/textRender.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("TextRendering") 2 | 3 | package com.jakewharton.picnic 4 | 5 | import com.jakewharton.crossword.TextCanvas 6 | import com.jakewharton.picnic.Table.PositionedCell 7 | import kotlin.jvm.JvmName 8 | import kotlin.jvm.JvmOverloads 9 | 10 | @Suppress("NOTHING_TO_INLINE", "UNUSED_PARAMETER") 11 | private inline fun debug(message: () -> String) { 12 | // println(message()) 13 | } 14 | 15 | @JvmOverloads 16 | @JvmName("render") 17 | fun Table.renderText( 18 | layoutFactory: (PositionedCell) -> TextLayout = ::SimpleLayout, 19 | border: TextBorder = TextBorder.DEFAULT, 20 | ): String { 21 | val layouts = positionedCells.associate { it.cell to layoutFactory(it) } 22 | 23 | debug { "Measure pass...\n 1/2..." } 24 | 25 | val columnWidths = IntArray(columnCount) { 1 } 26 | val columnBorderWidths = IntArray(columnCount + 1) 27 | val rowHeights = IntArray(rowCount) { 1 } 28 | val rowBorderHeights = IntArray(rowCount + 1) 29 | 30 | positionedCells.forEach { positionedCell -> 31 | val rowIndex = positionedCell.rowIndex 32 | val columnIndex = positionedCell.columnIndex 33 | val cell = positionedCell.cell 34 | val canonicalStyle = positionedCell.canonicalStyle 35 | 36 | val layout = layouts.getValue(cell) 37 | 38 | val columnSpan = cell.columnSpan 39 | if (columnSpan == 1) { 40 | val currentWidth = columnWidths[columnIndex] 41 | val contentWidth = layout.measureWidth() 42 | if (contentWidth > currentWidth) { 43 | debug { " ($rowIndex, $columnIndex) Column width $currentWidth -> $contentWidth" } 44 | columnWidths[columnIndex] = contentWidth 45 | } 46 | } 47 | 48 | val rowSpan = cell.rowSpan 49 | if (rowSpan == 1) { 50 | val currentHeight = rowHeights[rowIndex] 51 | val contentHeight = layout.measureHeight() 52 | if (contentHeight > currentHeight) { 53 | debug { " ($rowIndex, $columnIndex) Row height $currentHeight -> $contentHeight" } 54 | rowHeights[rowIndex] = contentHeight 55 | } 56 | } 57 | 58 | if ((columnIndex == 0 && tableStyle?.border == true || canonicalStyle?.borderLeft == true) && 59 | (columnIndex > 0 || tableStyle?.borderStyle != BorderStyle.Hidden) 60 | ) { 61 | debug { 62 | val oldValue = if (columnBorderWidths[columnIndex] == 0) "0 ->" else "already" 63 | " ($rowIndex, $columnIndex) Left border $oldValue 1" 64 | } 65 | columnBorderWidths[columnIndex] = 1 66 | } 67 | if ((columnIndex + columnSpan == columnCount && tableStyle?.border == true || canonicalStyle?.borderRight == true) && 68 | (columnIndex + columnSpan < columnCount || tableStyle?.borderStyle != BorderStyle.Hidden) 69 | ) { 70 | debug { 71 | val oldValue = if (columnBorderWidths[columnIndex + columnSpan] == 0) "0 ->" else "already" 72 | " ($rowIndex, $columnIndex) Right border $oldValue 1" 73 | } 74 | columnBorderWidths[columnIndex + columnSpan] = 1 75 | } 76 | if ((rowIndex == 0 && tableStyle?.border == true || canonicalStyle?.borderTop == true) && 77 | (rowIndex > 0 || tableStyle?.borderStyle != BorderStyle.Hidden) 78 | ) { 79 | debug { 80 | val oldValue = if (rowBorderHeights[rowIndex] == 0) "0 ->" else "already" 81 | " ($rowIndex, $columnIndex) Top border $oldValue 1" 82 | } 83 | rowBorderHeights[rowIndex] = 1 84 | } 85 | if ((rowIndex + rowSpan == rowCount && tableStyle?.border == true || canonicalStyle?.borderBottom == true) && 86 | (rowIndex + rowSpan < rowCount || tableStyle?.borderStyle != BorderStyle.Hidden) 87 | ) { 88 | debug { 89 | val oldValue = if (rowBorderHeights[rowIndex + rowSpan] == 0) "0 ->" else "already" 90 | " ($rowIndex, $columnIndex) Bottom border $oldValue 1" 91 | } 92 | rowBorderHeights[rowIndex + rowSpan] = 1 93 | } 94 | } 95 | debug { 96 | """ 97 | | Intermediate row heights: ${rowHeights.contentToString()} 98 | | Intermediate row border heights: ${rowBorderHeights.contentToString()} 99 | | Intermediate column widths: ${columnWidths.contentToString()} 100 | | Intermediate column border widths: ${columnBorderWidths.contentToString()} 101 | """.trimMargin() 102 | } 103 | 104 | debug { " 2/2..." } 105 | 106 | positionedCells.filter { it.cell.columnSpan > 1 } 107 | .sortedBy { it.cell.columnSpan } 108 | .forEach { positionedCell -> 109 | val rowIndex = positionedCell.rowIndex 110 | val columnIndex = positionedCell.columnIndex 111 | val cell = positionedCell.cell 112 | 113 | val layout = layouts.getValue(cell) 114 | val columnSpan = cell.columnSpan 115 | val contentWidth = layout.measureWidth() 116 | val columnSpanIndices = columnIndex until columnIndex + columnSpan 117 | val currentSpanColumnWidth = columnSpanIndices.sumOf { columnWidths[it] } 118 | val currentSpanBorderWidth = (columnIndex + 1 until columnIndex + columnSpan).sumOf { columnBorderWidths[it] } 119 | val currentSpanWidth = currentSpanColumnWidth + currentSpanBorderWidth 120 | val remainingSize = contentWidth - currentSpanWidth 121 | if (remainingSize > 0) { 122 | // TODO change to distribute remaining size proportionally to the existing widths? 123 | val commonSize = remainingSize / columnSpan 124 | val extraSize = remainingSize - (commonSize * columnSpan) 125 | columnSpanIndices.forEachIndexed { spanIndex, targetColumnIndex -> 126 | val additionalSize = if (spanIndex < extraSize) { 127 | commonSize + 1 128 | } else { 129 | commonSize 130 | } 131 | val currentWidth = columnWidths[targetColumnIndex] 132 | val newWidth = currentWidth + additionalSize 133 | debug { " ($rowIndex, $columnIndex) Increasing column $targetColumnIndex width from $currentWidth to $newWidth" } 134 | columnWidths[targetColumnIndex] = newWidth 135 | } 136 | } 137 | } 138 | 139 | positionedCells.filter { it.cell.rowSpan > 1 } 140 | .sortedBy { it.cell.rowSpan } 141 | .forEach { positionedCell -> 142 | val rowIndex = positionedCell.rowIndex 143 | val columnIndex = positionedCell.columnIndex 144 | val cell = positionedCell.cell 145 | 146 | val layout = layouts.getValue(cell) 147 | val rowSpan = cell.rowSpan 148 | val contentHeight = layout.measureHeight() 149 | val rowSpanIndices = rowIndex until rowIndex + rowSpan 150 | val currentSpanRowHeight = rowSpanIndices.sumOf { rowHeights[it] } 151 | val currentSpanBorderHeight = (rowIndex + 1 until rowIndex + rowSpan).sumOf { rowBorderHeights[it] } 152 | val currentSpanHeight = currentSpanRowHeight + currentSpanBorderHeight 153 | val remainingSize = contentHeight - currentSpanHeight 154 | if (remainingSize > 0) { 155 | // TODO change to distribute remaining size proportionally to the existing widths? 156 | val commonSize = remainingSize / rowSpan 157 | val extraSize = remainingSize - (commonSize * rowSpan) 158 | rowSpanIndices.forEachIndexed { spanIndex, targetRowIndex -> 159 | val additionalSize = if (spanIndex < extraSize) { 160 | commonSize + 1 161 | } else { 162 | commonSize 163 | } 164 | val currentHeight = rowHeights[targetRowIndex] 165 | val newHeight = currentHeight + additionalSize 166 | debug { " ($rowIndex, $columnIndex) Increasing row $targetRowIndex height from $currentHeight to $newHeight" } 167 | rowHeights[targetRowIndex] = newHeight 168 | } 169 | } 170 | } 171 | debug { 172 | """ 173 | | Final row heights: ${rowHeights.contentToString()} 174 | | Final row border heights: ${rowBorderHeights.contentToString()} 175 | | Final column widths: ${columnWidths.contentToString()} 176 | | Final column border widths: ${columnBorderWidths.contentToString()} 177 | """.trimMargin() 178 | } 179 | 180 | debug { "Layout pass..." } 181 | 182 | val tableLefts = IntArray(columnWidths.size + 1) 183 | val tableWidth: Int 184 | run { 185 | var left = 0 186 | for (i in columnWidths.indices) { 187 | tableLefts[i] = left 188 | left += columnWidths[i] + columnBorderWidths[i] 189 | } 190 | tableLefts[columnWidths.size] = left 191 | tableWidth = left + columnBorderWidths[columnWidths.size] 192 | } 193 | 194 | val tableTops = IntArray(rowHeights.size + 1) 195 | val tableHeight: Int 196 | run { 197 | var top = 0 198 | for (i in rowHeights.indices) { 199 | tableTops[i] = top 200 | top += rowHeights[i] + rowBorderHeights[i] 201 | } 202 | tableTops[rowHeights.size] = top 203 | tableHeight = top + rowBorderHeights[rowHeights.size] 204 | } 205 | debug { 206 | """ 207 | | Width: $tableWidth 208 | | Height: $tableHeight 209 | | Lefts: ${tableLefts.contentToString()} 210 | | Tops: ${tableTops.contentToString()} 211 | """.trimMargin() 212 | } 213 | 214 | debug { "Drawing pass..." } 215 | 216 | val canvas = TextCanvas(tableWidth, tableHeight) 217 | 218 | debug { " Borders..." } 219 | for (rowIndex in 0..rowCount) { 220 | val rowDrawStartIndex = tableTops[rowIndex] 221 | 222 | for (columnIndex in 0..columnCount) { 223 | val positionedCell = getOrNull(rowIndex, columnIndex) 224 | val cell = positionedCell?.cell 225 | val cellCanonicalStyle = positionedCell?.canonicalStyle 226 | 227 | val previousRowPositionedCell = getOrNull(rowIndex, columnIndex - 1) 228 | val previousRowCell = previousRowPositionedCell?.cell 229 | val previousRowCellCanonicalStyle = previousRowPositionedCell?.canonicalStyle 230 | 231 | val previousColumnPositionedCell = getOrNull(rowIndex - 1, columnIndex) 232 | val previousColumnCell = previousColumnPositionedCell?.cell 233 | val previousColumnCellCanonicalStyle = previousColumnPositionedCell?.canonicalStyle 234 | 235 | val columnDrawStartIndex = tableLefts[columnIndex] 236 | val rowBorderHeight = rowBorderHeights[rowIndex] 237 | val hasRowBorder = rowBorderHeight != 0 238 | val columnBorderWidth = columnBorderWidths[columnIndex] 239 | val hasColumnBorder = columnBorderWidth != 0 240 | if (hasRowBorder && hasColumnBorder) { 241 | val previousRowColumnPositionedCell = getOrNull(rowIndex - 1, columnIndex - 1) 242 | val previousRowColumnCell = previousRowColumnPositionedCell?.cell 243 | val previousRowColumnCellCanonicalStyle = previousRowColumnPositionedCell?.canonicalStyle 244 | 245 | val cornerTopBorder = previousRowColumnCell !== previousColumnCell && 246 | ( 247 | previousRowColumnCellCanonicalStyle?.borderRight == true || 248 | previousColumnCellCanonicalStyle?.borderLeft == true || 249 | rowIndex > 0 && (columnIndex == 0 || columnIndex == columnCount) && tableStyle?.border == true 250 | ) 251 | val cornerLeftBorder = previousRowColumnCell !== previousRowCell && 252 | ( 253 | previousRowColumnCellCanonicalStyle?.borderBottom == true || 254 | previousRowCellCanonicalStyle?.borderTop == true || 255 | columnIndex > 0 && (rowIndex == 0 || rowIndex == rowCount) && tableStyle?.border == true 256 | ) 257 | val cornerBottomBorder = previousRowCell !== cell && 258 | ( 259 | previousRowCellCanonicalStyle?.borderRight == true || 260 | cellCanonicalStyle?.borderLeft == true || 261 | rowIndex < rowCount && (columnIndex == 0 || columnIndex == columnCount) && tableStyle?.border == true 262 | ) 263 | val cornerRightBorder = previousColumnCell !== cell && 264 | ( 265 | previousColumnCellCanonicalStyle?.borderBottom == true || 266 | cellCanonicalStyle?.borderTop == true || 267 | columnIndex < columnCount && (rowIndex == 0 || rowIndex == rowCount) && tableStyle?.border == true 268 | ) 269 | if (cornerTopBorder || cornerLeftBorder || cornerBottomBorder || cornerRightBorder) { 270 | val borderChar = border.get( 271 | down = cornerBottomBorder, 272 | up = cornerTopBorder, 273 | left = cornerLeftBorder, 274 | right = cornerRightBorder, 275 | ) 276 | debug { " ($rowIndex, $columnIndex) corner '$borderChar': ($rowDrawStartIndex, $columnDrawStartIndex)" } 277 | canvas.write(rowDrawStartIndex, columnDrawStartIndex, borderChar) 278 | } 279 | } 280 | 281 | if (hasColumnBorder && 282 | previousRowCell !== cell && 283 | ( 284 | previousRowCellCanonicalStyle?.borderRight == true || 285 | cellCanonicalStyle?.borderLeft == true || 286 | (columnIndex == 0 || columnIndex == columnCount) && tableStyle?.border == true 287 | ) 288 | ) { 289 | val rowDrawEndIndex = tableTops[rowIndex + 1] // Safe given cell != null. 290 | val borderChar = border.vertical 291 | debug { " ($rowIndex, $columnIndex) left '$borderChar': (${rowDrawStartIndex + 1}, $columnDrawStartIndex) -> ($rowDrawEndIndex, $columnDrawStartIndex)" } 292 | for (rowDrawIndex in rowDrawStartIndex + rowBorderHeight until rowDrawEndIndex) { 293 | canvas.write(rowDrawIndex, columnDrawStartIndex, borderChar) 294 | } 295 | } 296 | 297 | if (hasRowBorder && 298 | previousColumnCell !== cell && 299 | ( 300 | previousColumnCellCanonicalStyle?.borderBottom == true || 301 | cellCanonicalStyle?.borderTop == true || 302 | (rowIndex == 0 || rowIndex == rowCount) && tableStyle?.border == true 303 | ) 304 | ) { 305 | val columnDrawEndIndex = tableLefts[columnIndex + 1] // Safe given cell != null 306 | val borderChar = border.horizontal 307 | debug { " ($rowIndex, $columnIndex) top '$borderChar': ($rowDrawStartIndex, ${columnDrawStartIndex + 1}) -> ($rowDrawStartIndex, $columnDrawEndIndex)" } 308 | for (columnDrawIndex in columnDrawStartIndex + columnBorderWidth until columnDrawEndIndex) { 309 | canvas.write(rowDrawStartIndex, columnDrawIndex, borderChar) 310 | } 311 | } 312 | } 313 | } 314 | 315 | debug { " Cells..." } 316 | positionedCells.forEach { positionedCell -> 317 | val rowIndex = positionedCell.rowIndex 318 | val columnIndex = positionedCell.columnIndex 319 | val cell = positionedCell.cell 320 | 321 | val cellLeft = tableLefts[columnIndex] + columnBorderWidths[columnIndex] 322 | val cellRight = tableLefts[columnIndex + cell.columnSpan] 323 | val cellTop = tableTops[rowIndex] + rowBorderHeights[rowIndex] 324 | val cellBottom = tableTops[rowIndex + cell.rowSpan] 325 | 326 | debug { 327 | """ 328 | | ($rowIndex, $columnIndex) clip: 329 | | horizontal [$cellLeft, $cellRight) 330 | | vertical [$cellTop, $cellBottom) 331 | """.trimMargin() 332 | } 333 | 334 | val clipped = canvas.clip(cellLeft, cellTop, cellRight, cellBottom) 335 | val layout = layouts.getValue(cell) 336 | layout.draw(clipped) 337 | } 338 | 339 | return canvas.toString() 340 | } 341 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/CellAlignmentTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import com.jakewharton.picnic.TextAlignment.BottomCenter 4 | import com.jakewharton.picnic.TextAlignment.BottomLeft 5 | import com.jakewharton.picnic.TextAlignment.BottomRight 6 | import com.jakewharton.picnic.TextAlignment.MiddleCenter 7 | import com.jakewharton.picnic.TextAlignment.MiddleLeft 8 | import com.jakewharton.picnic.TextAlignment.MiddleRight 9 | import com.jakewharton.picnic.TextAlignment.TopCenter 10 | import com.jakewharton.picnic.TextAlignment.TopLeft 11 | import com.jakewharton.picnic.TextAlignment.TopRight 12 | import kotlin.test.Test 13 | import kotlin.test.assertEquals 14 | 15 | class CellAlignmentTest { 16 | @Test fun alignmentsDoNotAffectSizing() { 17 | val table = table { 18 | row { 19 | cell("TL") { 20 | alignment = TopLeft 21 | } 22 | cell("TC") { 23 | alignment = TopCenter 24 | } 25 | cell("TR") { 26 | alignment = TopRight 27 | } 28 | } 29 | row { 30 | cell("ML") { 31 | alignment = MiddleLeft 32 | } 33 | cell("MC") { 34 | alignment = MiddleCenter 35 | } 36 | cell("MR") { 37 | alignment = MiddleRight 38 | } 39 | } 40 | row { 41 | cell("BL") { 42 | alignment = BottomLeft 43 | } 44 | cell("BC") { 45 | alignment = BottomCenter 46 | } 47 | cell("BR") { 48 | alignment = BottomRight 49 | } 50 | } 51 | } 52 | 53 | assertEquals( 54 | """ 55 | |TLTCTR 56 | |MLMCMR 57 | |BLBCBR 58 | """.trimMargin(), 59 | table.renderText(), 60 | ) 61 | } 62 | 63 | @Test fun alignmentsAndSizes() { 64 | val table = table { 65 | for (alignment in arrayOf(null) + TextAlignment.values()) { 66 | for (contentWidth in 1..3) { 67 | if (alignment == null && contentWidth != 1) continue 68 | for (contentHeight in 1..2) { 69 | if (alignment == null && contentHeight != 1) continue 70 | row { 71 | cell(alignment ?: "padding >\n\nalignment\n v") 72 | for (paddingLeft in 0..1) { 73 | for (paddingRight in 0..1) { 74 | for (paddingTop in 0..1) { 75 | for (paddingBottom in 0..1) { 76 | val text = if (alignment == null) { 77 | """ 78 | |l$paddingLeft 79 | |r$paddingRight 80 | |t$paddingTop 81 | |b$paddingBottom 82 | """.trimMargin() 83 | } else { 84 | ("X".repeat(contentWidth) + '\n').repeat(contentHeight).trimEnd() 85 | } 86 | cell(text) { 87 | border = true 88 | 89 | if (alignment == null) { 90 | this.alignment = TopLeft 91 | } else { 92 | this.paddingLeft = paddingLeft 93 | this.paddingRight = paddingRight 94 | this.paddingTop = paddingTop 95 | this.paddingBottom = paddingBottom 96 | this.alignment = alignment 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | assertEquals( 110 | """ 111 | | ┌───┬───┬───┬───┬────┬────┬────┬────┬────┬────┬────┬────┬─────┬─────┬─────┬─────┐ 112 | |padding > │l0 │l0 │l0 │l0 │l0 │l0 │l0 │l0 │l1 │l1 │l1 │l1 │l1 │l1 │l1 │l1 │ 113 | | │r0 │r0 │r0 │r0 │r1 │r1 │r1 │r1 │r0 │r0 │r0 │r0 │r1 │r1 │r1 │r1 │ 114 | |alignment │t0 │t0 │t1 │t1 │t0 │t0 │t1 │t1 │t0 │t0 │t1 │t1 │t0 │t0 │t1 │t1 │ 115 | | v │b0 │b1 │b0 │b1 │b0 │b1 │b0 │b1 │b0 │b1 │b0 │b1 │b0 │b1 │b0 │b1 │ 116 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 117 | |TopLeft │X │X │ │ │X │X │ │ │ X │ X │ │ │ X │ X │ │ │ 118 | | │ │ │X │X │ │ │X │X │ │ │ X │ X │ │ │ X │ X │ 119 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 120 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 121 | |TopLeft │X │X │ │ │X │X │ │ │ X │ X │ │ │ X │ X │ │ │ 122 | | │X │X │X │X │X │X │X │X │ X │ X │ X │ X │ X │ X │ X │ X │ 123 | | │ │ │X │X │ │ │X │X │ │ │ X │ X │ │ │ X │ X │ 124 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 125 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 126 | |TopLeft │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ │ │ 127 | | │ │ │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ 128 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 129 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 130 | |TopLeft │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ │ │ 131 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 132 | | │ │ │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ 133 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 134 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 135 | |TopLeft │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 136 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 137 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 138 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 139 | |TopLeft │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 140 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 141 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 142 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 143 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 144 | |TopCenter │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ 145 | | │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ 146 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 147 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 148 | |TopCenter │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ 149 | | │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ 150 | | │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ │ │ X │ X │ 151 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 152 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 153 | |TopCenter │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ │ │ 154 | | │ │ │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ 155 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 156 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 157 | |TopCenter │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ │ │ 158 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 159 | | │ │ │XX │XX │ │ │XX │XX │ │ │ XX │ XX │ │ │ XX │ XX │ 160 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 161 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 162 | |TopCenter │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 163 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 164 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 165 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 166 | |TopCenter │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 167 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 168 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 169 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 170 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 171 | |TopRight │ X│ X│ │ │ X │ X │ │ │ X│ X│ │ │ X │ X │ │ │ 172 | | │ │ │ X│ X│ │ │ X │ X │ │ │ X│ X│ │ │ X │ X │ 173 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 174 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 175 | |TopRight │ X│ X│ │ │ X │ X │ │ │ X│ X│ │ │ X │ X │ │ │ 176 | | │ X│ X│ X│ X│ X │ X │ X │ X │ X│ X│ X│ X│ X │ X │ X │ X │ 177 | | │ │ │ X│ X│ │ │ X │ X │ │ │ X│ X│ │ │ X │ X │ 178 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 179 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 180 | |TopRight │ XX│ XX│ │ │ XX │ XX │ │ │ XX│ XX│ │ │ XX │ XX │ │ │ 181 | | │ │ │ XX│ XX│ │ │ XX │ XX │ │ │ XX│ XX│ │ │ XX │ XX │ 182 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 183 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 184 | |TopRight │ XX│ XX│ │ │ XX │ XX │ │ │ XX│ XX│ │ │ XX │ XX │ │ │ 185 | | │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ 186 | | │ │ │ XX│ XX│ │ │ XX │ XX │ │ │ XX│ XX│ │ │ XX │ XX │ 187 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 188 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 189 | |TopRight │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 190 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 191 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 192 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 193 | |TopRight │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ │ │ 194 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 195 | | │ │ │XXX│XXX│ │ │XXX │XXX │ │ │ XXX│ XXX│ │ │ XXX │ XXX │ 196 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 197 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 198 | |MiddleLeft │ │X │ │ │ │X │ │ │ │ X │ │ │ │ X │ │ │ 199 | | │X │ │X │X │X │ │X │X │ X │ │ X │ X │ X │ │ X │ X │ 200 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 201 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 202 | |MiddleLeft │ │X │ │ │ │X │ │ │ │ X │ │ │ │ X │ │ │ 203 | | │X │X │X │X │X │X │X │X │ X │ X │ X │ X │ X │ X │ X │ X │ 204 | | │X │ │X │X │X │ │X │X │ X │ │ X │ X │ X │ │ X │ X │ 205 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 206 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 207 | |MiddleLeft │ │XX │ │ │ │XX │ │ │ │ XX │ │ │ │ XX │ │ │ 208 | | │XX │ │XX │XX │XX │ │XX │XX │ XX │ │ XX │ XX │ XX │ │ XX │ XX │ 209 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 210 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 211 | |MiddleLeft │ │XX │ │ │ │XX │ │ │ │ XX │ │ │ │ XX │ │ │ 212 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 213 | | │XX │ │XX │XX │XX │ │XX │XX │ XX │ │ XX │ XX │ XX │ │ XX │ XX │ 214 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 215 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 216 | |MiddleLeft │ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 217 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 218 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 219 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 220 | |MiddleLeft │ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 221 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 222 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 223 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 224 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 225 | |MiddleCenter│ │ X │ │ │ │ X │ │ │ │ X │ │ │ │ X │ │ │ 226 | | │ X │ │ X │ X │ X │ │ X │ X │ X │ │ X │ X │ X │ │ X │ X │ 227 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 228 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 229 | |MiddleCenter│ │ X │ │ │ │ X │ │ │ │ X │ │ │ │ X │ │ │ 230 | | │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ 231 | | │ X │ │ X │ X │ X │ │ X │ X │ X │ │ X │ X │ X │ │ X │ X │ 232 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 233 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 234 | |MiddleCenter│ │XX │ │ │ │XX │ │ │ │ XX │ │ │ │ XX │ │ │ 235 | | │XX │ │XX │XX │XX │ │XX │XX │ XX │ │ XX │ XX │ XX │ │ XX │ XX │ 236 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 237 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 238 | |MiddleCenter│ │XX │ │ │ │XX │ │ │ │ XX │ │ │ │ XX │ │ │ 239 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 240 | | │XX │ │XX │XX │XX │ │XX │XX │ XX │ │ XX │ XX │ XX │ │ XX │ XX │ 241 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 242 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 243 | |MiddleCenter│ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 244 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 245 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 246 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 247 | |MiddleCenter│ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 248 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 249 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 250 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 251 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 252 | |MiddleRight │ │ X│ │ │ │ X │ │ │ │ X│ │ │ │ X │ │ │ 253 | | │ X│ │ X│ X│ X │ │ X │ X │ X│ │ X│ X│ X │ │ X │ X │ 254 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 255 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 256 | |MiddleRight │ │ X│ │ │ │ X │ │ │ │ X│ │ │ │ X │ │ │ 257 | | │ X│ X│ X│ X│ X │ X │ X │ X │ X│ X│ X│ X│ X │ X │ X │ X │ 258 | | │ X│ │ X│ X│ X │ │ X │ X │ X│ │ X│ X│ X │ │ X │ X │ 259 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 260 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 261 | |MiddleRight │ │ XX│ │ │ │ XX │ │ │ │ XX│ │ │ │ XX │ │ │ 262 | | │ XX│ │ XX│ XX│ XX │ │ XX │ XX │ XX│ │ XX│ XX│ XX │ │ XX │ XX │ 263 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 264 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 265 | |MiddleRight │ │ XX│ │ │ │ XX │ │ │ │ XX│ │ │ │ XX │ │ │ 266 | | │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ 267 | | │ XX│ │ XX│ XX│ XX │ │ XX │ XX │ XX│ │ XX│ XX│ XX │ │ XX │ XX │ 268 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 269 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 270 | |MiddleRight │ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 271 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 272 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 273 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 274 | |MiddleRight │ │XXX│ │ │ │XXX │ │ │ │ XXX│ │ │ │ XXX │ │ │ 275 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 276 | | │XXX│ │XXX│XXX│XXX │ │XXX │XXX │ XXX│ │ XXX│ XXX│ XXX │ │ XXX │ XXX │ 277 | | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 278 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 279 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 280 | | │ │X │ │X │ │X │ │X │ │ X │ │ X │ │ X │ │ X │ 281 | | │X │ │X │ │X │ │X │ │ X │ │ X │ │ X │ │ X │ │ 282 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 283 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 284 | | │ │X │ │X │ │X │ │X │ │ X │ │ X │ │ X │ │ X │ 285 | | │X │X │X │X │X │X │X │X │ X │ X │ X │ X │ X │ X │ X │ X │ 286 | | │X │ │X │ │X │ │X │ │ X │ │ X │ │ X │ │ X │ │ 287 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 288 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 289 | | │ │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ 290 | | │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ │ 291 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 292 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 293 | | │ │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ 294 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 295 | | │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ │ 296 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 297 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 298 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 299 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 300 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 301 | |BottomLeft │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 302 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 303 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 304 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 305 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 306 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 307 | | │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ 308 | | │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ 309 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 310 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 311 | | │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ 312 | | │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ X │ 313 | | │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ X │ │ 314 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 315 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 316 | | │ │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ 317 | | │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ │ 318 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 319 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 320 | | │ │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ 321 | | │XX │XX │XX │XX │XX │XX │XX │XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ XX │ 322 | | │XX │ │XX │ │XX │ │XX │ │ XX │ │ XX │ │ XX │ │ XX │ │ 323 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 324 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 325 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 326 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 327 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 328 | |BottomCenter│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 329 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 330 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 331 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 332 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 333 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 334 | | │ │ X│ │ X│ │ X │ │ X │ │ X│ │ X│ │ X │ │ X │ 335 | | │ X│ │ X│ │ X │ │ X │ │ X│ │ X│ │ X │ │ X │ │ 336 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 337 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 338 | | │ │ X│ │ X│ │ X │ │ X │ │ X│ │ X│ │ X │ │ X │ 339 | | │ X│ X│ X│ X│ X │ X │ X │ X │ X│ X│ X│ X│ X │ X │ X │ X │ 340 | | │ X│ │ X│ │ X │ │ X │ │ X│ │ X│ │ X │ │ X │ │ 341 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 342 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 343 | | │ │ XX│ │ XX│ │ XX │ │ XX │ │ XX│ │ XX│ │ XX │ │ XX │ 344 | | │ XX│ │ XX│ │ XX │ │ XX │ │ XX│ │ XX│ │ XX │ │ XX │ │ 345 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 346 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 347 | | │ │ XX│ │ XX│ │ XX │ │ XX │ │ XX│ │ XX│ │ XX │ │ XX │ 348 | | │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ XX│ XX│ XX│ XX│ XX │ XX │ XX │ XX │ 349 | | │ XX│ │ XX│ │ XX │ │ XX │ │ XX│ │ XX│ │ XX │ │ XX │ │ 350 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 351 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 352 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 353 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 354 | | ├───┼───┼───┼───┼────┼────┼────┼────┼────┼────┼────┼────┼─────┼─────┼─────┼─────┤ 355 | |BottomRight │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 356 | | │ │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ 357 | | │XXX│XXX│XXX│XXX│XXX │XXX │XXX │XXX │ XXX│ XXX│ XXX│ XXX│ XXX │ XXX │ XXX │ XXX │ 358 | | │XXX│ │XXX│ │XXX │ │XXX │ │ XXX│ │ XXX│ │ XXX │ │ XXX │ │ 359 | | └───┴───┴───┴───┴────┴────┴────┴────┴────┴────┴────┴────┴─────┴─────┴─────┴─────┘ 360 | """.trimMargin(), 361 | table.renderText(), 362 | ) 363 | } 364 | 365 | @Test fun multipleLinesAlignedIndividually() { 366 | val table = table { 367 | row { 368 | for (alignment in listOf(TopLeft, TopCenter, TopRight)) { 369 | cell("X\nXXX\nXXXXX\nXXX\nX") { 370 | this.alignment = alignment 371 | } 372 | } 373 | } 374 | } 375 | 376 | assertEquals( 377 | """ 378 | |X X X 379 | |XXX XXX XXX 380 | |XXXXXXXXXXXXXXX 381 | |XXX XXX XXX 382 | |X X X 383 | """.trimMargin(), 384 | table.renderText(), 385 | ) 386 | } 387 | 388 | @Test fun stylePropagation() { 389 | val table = table { 390 | cellStyle { 391 | alignment = TopLeft 392 | } 393 | header { 394 | row("0", "123", "456") 395 | } 396 | body { 397 | cellStyle { 398 | alignment = BottomLeft 399 | } 400 | row { 401 | cellStyle { 402 | alignment = TopRight 403 | } 404 | cell("1\n2\n3") 405 | cell("TR") 406 | cell("BR") { 407 | alignment = BottomRight 408 | } 409 | } 410 | row("4\n5\n6", "BL") 411 | } 412 | footer { 413 | row("7\n8\n9", "TL") 414 | } 415 | } 416 | 417 | assertEquals( 418 | """ 419 | |0123456 420 | |1 TR 421 | |2 422 | |3 BR 423 | |4 424 | |5 425 | |6BL 426 | |7TL 427 | |8 428 | |9 429 | """.trimMargin(), 430 | table.renderText(), 431 | ) 432 | } 433 | 434 | @Test fun displaySizeUsedForAlignment() { 435 | val table = table { 436 | cellStyle { 437 | alignment = MiddleCenter 438 | } 439 | row("\u001B[31;1;4mHello\u001B[0m", "a") 440 | row("", "a") 441 | } 442 | 443 | assertEquals( 444 | """ 445 | | $esc[31;1;4mHello$esc[0m a 446 | |a 447 | """.trimMargin(), 448 | table.renderText(), 449 | ) 450 | } 451 | 452 | @Test fun singleCellAlignmentWithInvisibleChars() { 453 | val table = table { 454 | cellStyle { 455 | alignment = MiddleCenter 456 | } 457 | row("\u001B[31;1;4mHello\u001B[0m\n\u001B[31;1;4mHello12\u001B[0m\nHello", "a\na\na") 458 | } 459 | 460 | assertEquals( 461 | """ 462 | | $esc[31;1;4mHello$esc[0m a 463 | |$esc[31;1;4mHello12$esc[0ma 464 | | Hello a 465 | """.trimMargin(), 466 | table.renderText(), 467 | ) 468 | } 469 | } 470 | 471 | private const val esc = "\u001B" 472 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/CellBorderTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class CellBorderTest { 7 | @Test fun allCorners() { 8 | val table = table { 9 | row { 10 | cell(" ") { 11 | borderTop = true 12 | borderLeft = true 13 | borderRight = true 14 | } 15 | cell(" ") { 16 | borderTop = true 17 | borderLeft = true 18 | borderRight = true 19 | } 20 | cell(" ") { 21 | borderLeft = true 22 | borderRight = true 23 | } 24 | } 25 | row { 26 | cell(" ") { 27 | borderTop = true 28 | borderLeft = true 29 | borderRight = true 30 | borderBottom = true 31 | } 32 | cell(" ") { 33 | borderLeft = true 34 | borderBottom = true 35 | } 36 | cell(" ") { 37 | borderRight = true 38 | borderBottom = true 39 | } 40 | } 41 | row { 42 | cell(" ") { 43 | borderTop = true 44 | borderRight = true 45 | borderBottom = true 46 | } 47 | cell(" ") { 48 | borderTop = true 49 | borderLeft = true 50 | borderBottom = true 51 | } 52 | cell(" ") { 53 | borderTop = true 54 | } 55 | } 56 | } 57 | 58 | assertEquals( 59 | """ 60 | |┌─┬─┐ ╷ 61 | |│ │ │ │ 62 | |├─┤ ╵ │ 63 | |│ │ │ 64 | |└─┼───┘ 65 | | │ 66 | |╶─┴─╴ 67 | """.trimMargin(), 68 | table.renderText(), 69 | ) 70 | 71 | assertEquals( 72 | """ 73 | |╭─┬─╮ ╷ 74 | |│ │ │ │ 75 | |├─┤ ╵ │ 76 | |│ │ │ 77 | |╰─┼───╯ 78 | | │ 79 | |╶─┴─╴ 80 | """.trimMargin(), 81 | table.renderText(border = TextBorder.ROUNDED), 82 | ) 83 | 84 | assertEquals( 85 | """ 86 | |+-+-+ 87 | || | | | 88 | |+-+ | 89 | || | | 90 | |+-+---+ 91 | | | 92 | | -+- 93 | """.trimMargin(), 94 | table.renderText(border = TextBorder.ASCII), 95 | ) 96 | } 97 | 98 | @Test fun adjacentRowBordersWithoutCorners() { 99 | val table = table { 100 | row { 101 | cell(1) { 102 | borderBottom = true 103 | } 104 | cell(2) { 105 | borderBottom = true 106 | } 107 | } 108 | row(3, 4) 109 | } 110 | 111 | assertEquals( 112 | """ 113 | |12 114 | |── 115 | |34 116 | """.trimMargin(), 117 | table.renderText(), 118 | ) 119 | } 120 | 121 | @Test fun adjacentColumnBordersWithoutCorners() { 122 | val table = table { 123 | row { 124 | cell(1) { 125 | borderRight = true 126 | } 127 | cell(2) 128 | } 129 | row { 130 | cell(3) { 131 | borderRight = true 132 | } 133 | cell(4) 134 | } 135 | } 136 | 137 | assertEquals( 138 | """ 139 | |1│2 140 | |3│4 141 | """.trimMargin(), 142 | table.renderText(), 143 | ) 144 | } 145 | 146 | @Test fun rowSpanPushesBordersToTheRight() { 147 | val table = table { 148 | row { 149 | cell("A") { 150 | rowSpan = 2 151 | borderBottom = true 152 | } 153 | cell("B") { 154 | borderBottom = true 155 | } 156 | } 157 | row { 158 | cell("C") { 159 | borderBottom = true 160 | } 161 | } 162 | } 163 | 164 | assertEquals( 165 | """ 166 | |AB 167 | | ─ 168 | | C 169 | |── 170 | """.trimMargin(), 171 | table.renderText(), 172 | ) 173 | } 174 | 175 | @Test fun stylePropagation() { 176 | val table = table { 177 | cellStyle { 178 | border = true 179 | } 180 | body { 181 | cellStyle { 182 | borderTop = false 183 | } 184 | row { 185 | cellStyle { 186 | borderLeft = false 187 | } 188 | cell("A") { 189 | borderRight = false 190 | } 191 | } 192 | } 193 | } 194 | 195 | assertEquals( 196 | """ 197 | |A 198 | |─ 199 | """.trimMargin(), 200 | table.renderText(), 201 | ) 202 | } 203 | 204 | @Test fun tableStyleTakesPrecedenceOverCell() { 205 | val table = table { 206 | style { 207 | borderStyle = BorderStyle.Hidden 208 | } 209 | cellStyle { 210 | border = true 211 | } 212 | body { 213 | row("A", "B", "C") 214 | row("D", "E", "F") 215 | row("G", "H", "I") 216 | } 217 | } 218 | 219 | assertEquals( 220 | """ 221 | |A│B│C 222 | |─┼─┼─ 223 | |D│E│F 224 | |─┼─┼─ 225 | |G│H│I 226 | """.trimMargin(), 227 | table.renderText(), 228 | ) 229 | } 230 | 231 | @Test fun tableStyleTakesPrecedenceOverCellWithRowAndColumnSpans() { 232 | val table = table { 233 | style { 234 | borderStyle = BorderStyle.Hidden 235 | } 236 | cellStyle { 237 | border = true 238 | } 239 | row { 240 | cell("1") 241 | cell("1") 242 | cell("2") { 243 | rowSpan = 2 244 | columnSpan = 2 245 | } 246 | } 247 | row("1", "1") 248 | row { 249 | cell("2") { 250 | rowSpan = 2 251 | columnSpan = 2 252 | } 253 | cell("1") 254 | cell("1") 255 | } 256 | row("1", "1") 257 | } 258 | 259 | assertEquals( 260 | """ 261 | |1│1│2 262 | |─┼─┤ 263 | |1│1│ 264 | |─┴─┼─┬─ 265 | |2 │1│1 266 | | ├─┼─ 267 | | │1│1 268 | """.trimMargin(), 269 | table.renderText(), 270 | ) 271 | } 272 | 273 | @Test fun borderLeftCalculationWithTableBorderHidden() { 274 | val table = table { 275 | style { 276 | borderStyle = BorderStyle.Hidden 277 | } 278 | cellStyle { 279 | borderLeft = true 280 | } 281 | row("1", "2", "3") 282 | row { 283 | cell("4") { 284 | columnSpan = 2 285 | } 286 | cell("5") 287 | } 288 | row { 289 | cell("6") 290 | cell("7") { 291 | columnSpan = 2 292 | } 293 | } 294 | row { 295 | cell("8") { 296 | columnSpan = 3 297 | } 298 | } 299 | } 300 | 301 | assertEquals( 302 | """ 303 | |1│2│3 304 | |4 │5 305 | |6│7 306 | |8 307 | """.trimMargin(), 308 | table.renderText(), 309 | ) 310 | } 311 | 312 | @Test fun borderRightCalculationWithTableBorderHidden() { 313 | val table = table { 314 | style { 315 | borderStyle = BorderStyle.Hidden 316 | } 317 | cellStyle { 318 | borderRight = true 319 | } 320 | row("1", "2", "3") 321 | row { 322 | cell("4") { 323 | columnSpan = 2 324 | } 325 | cell("5") 326 | } 327 | row { 328 | cell("6") 329 | cell("7") { 330 | columnSpan = 2 331 | } 332 | } 333 | row { 334 | cell("8") { 335 | columnSpan = 3 336 | } 337 | } 338 | } 339 | 340 | assertEquals( 341 | """ 342 | |1│2│3 343 | |4 │5 344 | |6│7 345 | |8 346 | """.trimMargin(), 347 | table.renderText(), 348 | ) 349 | } 350 | 351 | @Test fun borderTopCalculationWithTableBorderHidden() { 352 | val table = table { 353 | style { 354 | borderStyle = BorderStyle.Hidden 355 | } 356 | cellStyle { 357 | borderTop = true 358 | } 359 | row { 360 | cell("1") 361 | cell("2") { 362 | rowSpan = 2 363 | } 364 | cell("3") 365 | cell("4") { 366 | rowSpan = 3 367 | } 368 | } 369 | row { 370 | cell("5") 371 | cell("6") { 372 | rowSpan = 2 373 | } 374 | } 375 | row("7", "8") 376 | } 377 | 378 | assertEquals( 379 | """ 380 | |1234 381 | |─ ─ 382 | |5 6 383 | |── 384 | |78 385 | """.trimMargin(), 386 | table.renderText(), 387 | ) 388 | } 389 | 390 | @Test fun borderBottomCalculationWithTableBorderHidden() { 391 | val table = table { 392 | style { 393 | borderStyle = BorderStyle.Hidden 394 | } 395 | cellStyle { 396 | borderBottom = true 397 | } 398 | row { 399 | cell("1") 400 | cell("2") { 401 | rowSpan = 2 402 | } 403 | cell("3") 404 | cell("4") { 405 | rowSpan = 3 406 | } 407 | } 408 | row { 409 | cell("5") 410 | cell("6") { 411 | rowSpan = 2 412 | } 413 | } 414 | row("7", "8") 415 | } 416 | 417 | assertEquals( 418 | """ 419 | |1234 420 | |─ ─ 421 | |5 6 422 | |── 423 | |78 424 | """.trimMargin(), 425 | table.renderText(), 426 | ) 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/CellSizeTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class CellSizeTest { 7 | @Test fun height() { 8 | val table = table { 9 | row("1\n2\n3", "1\n2", "1") 10 | row("1\n2", "1", "1\n2\n3") 11 | row("1", "1\n2\n3", "1\n2") 12 | } 13 | 14 | assertEquals( 15 | """ 16 | |111 17 | |22 18 | |3 19 | |111 20 | |2 2 21 | | 3 22 | |111 23 | | 22 24 | | 3 25 | """.trimMargin(), 26 | table.renderText(), 27 | ) 28 | } 29 | 30 | @Test fun heightWithVerticalPadding() { 31 | val table = table { 32 | row { 33 | cell(1) 34 | cell(2) { 35 | paddingTop = 1 36 | } 37 | cell(3) { 38 | paddingBottom = 1 39 | } 40 | } 41 | row { 42 | cell(1) 43 | cell(2) { 44 | paddingTop = 1 45 | paddingBottom = 1 46 | } 47 | cell(3) { 48 | paddingTop = 1 49 | paddingBottom = 1 50 | } 51 | } 52 | } 53 | 54 | assertEquals( 55 | """ 56 | |1 3 57 | | 2 58 | |1 59 | | 23 60 | | 61 | """.trimMargin(), 62 | table.renderText(), 63 | ) 64 | } 65 | 66 | @Test fun width() { 67 | val table = table { 68 | row("123", "12", "1") 69 | row("12", "1", "123") 70 | row("1", "123", "12") 71 | } 72 | 73 | assertEquals( 74 | """ 75 | |12312 1 76 | |12 1 123 77 | |1 12312 78 | """.trimMargin(), 79 | table.renderText(), 80 | ) 81 | } 82 | 83 | @Test fun widthWithHorizontalPadding() { 84 | val table = table { 85 | row { 86 | cell(1) 87 | cell(2) { 88 | paddingLeft = 2 89 | } 90 | cell(3) { 91 | paddingRight = 2 92 | } 93 | } 94 | row { 95 | cell(1) 96 | cell(2) { 97 | paddingLeft = 1 98 | paddingRight = 1 99 | } 100 | cell(3) { 101 | paddingLeft = 1 102 | paddingRight = 1 103 | } 104 | } 105 | } 106 | 107 | assertEquals( 108 | """ 109 | |1 23 110 | |1 2 3 111 | """.trimMargin(), 112 | table.renderText(), 113 | ) 114 | } 115 | 116 | @Test fun widthAndHeight() { 117 | val table = table { 118 | row("123\n12\n1", "12\n1", "1") 119 | row("12\n1", "1", "123\n12\n1") 120 | row("1", "123\n12\n1", "12\n1") 121 | } 122 | 123 | assertEquals( 124 | """ 125 | |12312 1 126 | |12 1 127 | |1 128 | |12 1 123 129 | |1 12 130 | | 1 131 | |1 12312 132 | | 12 1 133 | | 1 134 | """.trimMargin(), 135 | table.renderText(), 136 | ) 137 | } 138 | 139 | @Test fun unicode() { 140 | val table = table { 141 | // 1 UTF-8 bytes. 142 | row('\u0031', 'a') 143 | // 2 UTF-8 bytes. 144 | row('\u00A3', 'a') 145 | // 3 UTF-8 bytes. 146 | row('\u20AC', 'a') 147 | // 3 UTF-8 bytes, full-width. 148 | row('\u5317', 'a') 149 | // 4 UTF-8 bytes (2 * UTF-16), full-width. 150 | row("\uD83D\uDE03", 'a') 151 | } 152 | 153 | assertEquals( 154 | """ 155 | |1a 156 | |£a 157 | |€a 158 | |北a 159 | |😃a 160 | """.trimMargin(), 161 | table.renderText(), 162 | ) 163 | } 164 | 165 | @Test fun mixedWidth() { 166 | // Rows contain mixture of BMP and supplementary codepoints. 167 | val table = table { 168 | row("a", "a") 169 | // 2 UTF-8 bytes. 170 | row("😃.😃.😃", 'a') 171 | // 2 UTF-8 bytes. 172 | row(".😃.😃.", 'a') 173 | } 174 | 175 | assertEquals( 176 | """ 177 | |a a 178 | |😃.😃.😃a 179 | |.😃.😃.a 180 | """.trimMargin(), 181 | table.renderText(), 182 | ) 183 | } 184 | 185 | @Test fun asniEscapeCodesAreNotMeasured() { 186 | val table = table { 187 | row("a") 188 | row("\u001B[31;1;4ma\u001B[0m") 189 | } 190 | assertEquals(1, table.columnCount) 191 | 192 | assertEquals( 193 | """ 194 | |a 195 | |$esc[31;1;4ma$esc[0m 196 | """.trimMargin(), 197 | table.renderText(), 198 | ) 199 | } 200 | } 201 | 202 | private const val esc = "\u001B" 203 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/CellSpanTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertNotNull 6 | import kotlin.test.assertNull 7 | 8 | class CellSpanTest { 9 | @Test fun columnSpans() { 10 | val table = table { 11 | row { 12 | cell("88888888") { 13 | columnSpan = 8 14 | } 15 | cell("1") 16 | } 17 | row { 18 | repeat(2) { 19 | cell("4444") { 20 | columnSpan = 4 21 | } 22 | } 23 | cell("1") 24 | } 25 | row { 26 | repeat(4) { 27 | cell("22") { 28 | columnSpan = 2 29 | } 30 | } 31 | cell("1") 32 | } 33 | row { 34 | repeat(9) { 35 | cell("1") 36 | } 37 | } 38 | } 39 | 40 | assertEquals( 41 | """ 42 | |888888881 43 | |444444441 44 | |222222221 45 | |111111111 46 | """.trimMargin(), 47 | table.renderText(), 48 | ) 49 | } 50 | 51 | @Test fun columnSpanAcrossDifferentSizedColumnsDoesNotExpand() { 52 | val table = table { 53 | row("1", "22", "333") 54 | row { 55 | cell("666666") { 56 | columnSpan = 3 57 | } 58 | } 59 | } 60 | 61 | assertEquals( 62 | """ 63 | |122333 64 | |666666 65 | """.trimMargin(), 66 | table.renderText(), 67 | ) 68 | } 69 | 70 | @Test fun columnSpanAcrossBorderDoesNotExpand() { 71 | val table = table { 72 | row { 73 | cell("11") { 74 | borderRight = true 75 | } 76 | cell("22") 77 | } 78 | row { 79 | cell("33333") { 80 | columnSpan = 2 81 | } 82 | } 83 | } 84 | 85 | assertEquals( 86 | """ 87 | |11│22 88 | |33333 89 | """.trimMargin(), 90 | table.renderText(), 91 | ) 92 | } 93 | 94 | @Test fun rowSpans() { 95 | val table = table { 96 | row { 97 | cell("8\n8\n8\n8\n8\n8\n8\n8") { 98 | rowSpan = 8 99 | } 100 | cell("4\n4\n4\n4") { 101 | rowSpan = 4 102 | } 103 | cell("2\n2") { 104 | rowSpan = 2 105 | } 106 | cell("1") 107 | } 108 | row("1") 109 | row { 110 | cell("2\n2") { 111 | rowSpan = 2 112 | } 113 | cell("1") 114 | } 115 | row("1") 116 | row { 117 | cell("4\n4\n4\n4") { 118 | rowSpan = 4 119 | } 120 | cell("2\n2") { 121 | rowSpan = 2 122 | } 123 | cell("1") 124 | } 125 | row("1") 126 | row { 127 | cell("2\n2") { 128 | rowSpan = 2 129 | } 130 | cell("1") 131 | } 132 | row("1") 133 | row { 134 | repeat(4) { 135 | cell("1") 136 | } 137 | } 138 | } 139 | 140 | assertEquals( 141 | """ 142 | |8421 143 | |8421 144 | |8421 145 | |8421 146 | |8421 147 | |8421 148 | |8421 149 | |8421 150 | |1111 151 | """.trimMargin(), 152 | table.renderText(), 153 | ) 154 | } 155 | 156 | @Test fun rowSpanAcrossDifferentSizedRowsDoesNotExpand() { 157 | val table = table { 158 | row { 159 | cell("6\n6\n6\n6\n6\n6") { 160 | rowSpan = 3 161 | } 162 | cell("1") 163 | } 164 | row("2\n2") 165 | row("3\n3\n3") 166 | } 167 | 168 | assertEquals( 169 | """ 170 | |61 171 | |62 172 | |62 173 | |63 174 | |63 175 | |63 176 | """.trimMargin(), 177 | table.renderText(), 178 | ) 179 | } 180 | 181 | @Test fun rowSpanAcrossBorderDoesNotExpand() { 182 | val table = table { 183 | row { 184 | cell("1\n1\n1") { 185 | rowSpan = 2 186 | } 187 | cell("2") { 188 | borderBottom = true 189 | } 190 | } 191 | row("3") 192 | } 193 | 194 | assertEquals( 195 | """ 196 | |12 197 | |1─ 198 | |13 199 | """.trimMargin(), 200 | table.renderText(), 201 | ) 202 | } 203 | 204 | @Test fun rowAndColumnSpans() { 205 | val table = table { 206 | row { 207 | cell("333\n333\n333") { 208 | rowSpan = 3 209 | columnSpan = 3 210 | } 211 | cell("1") 212 | cell("1") 213 | cell("1") 214 | } 215 | row { 216 | cell("22\n22") { 217 | rowSpan = 2 218 | columnSpan = 2 219 | } 220 | cell("1") 221 | } 222 | row("1") 223 | row { 224 | cell("22\n22") { 225 | rowSpan = 2 226 | columnSpan = 2 227 | } 228 | cell("333\n333\n333") { 229 | rowSpan = 3 230 | columnSpan = 3 231 | } 232 | cell("1") 233 | } 234 | row("1") 235 | row("1", "1", "1") 236 | row("1", "1", "1", "1", "1", "1") 237 | } 238 | 239 | assertEquals( 240 | """ 241 | |333111 242 | |333221 243 | |333221 244 | |223331 245 | |223331 246 | |113331 247 | |111111 248 | """.trimMargin(), 249 | table.renderText(), 250 | ) 251 | } 252 | 253 | @Test fun rowSpanAtEndOfRow() { 254 | // This test ensures that row span carries at the end of a row are decremented properly 255 | // even when there are no remaining cells. 256 | 257 | val table = table { 258 | row { 259 | cell("1") 260 | cell("2\n2") { 261 | rowSpan = 2 262 | } 263 | } 264 | row("1") 265 | row("1", "1") 266 | } 267 | 268 | assertEquals( 269 | """ 270 | |12 271 | |12 272 | |11 273 | """.trimMargin(), 274 | table.renderText(), 275 | ) 276 | } 277 | 278 | @Test fun rowSpanLeaveHole() { 279 | val table = table { 280 | row { 281 | cell("1") 282 | cell("2\n2") { 283 | rowSpan = 2 284 | } 285 | cell("1") 286 | cell("2\n2") { 287 | rowSpan = 2 288 | } 289 | } 290 | row("1") 291 | row("1", "1", "1", "1") 292 | } 293 | 294 | assertEquals( 295 | """ 296 | |1212 297 | |12 2 298 | |1111 299 | """.trimMargin(), 300 | table.renderText(), 301 | ) 302 | assertNull(table.getOrNull(1, 2)) 303 | assertNotNull(table.getOrNull(1, 3)) 304 | } 305 | 306 | @Test fun rowSpanPartialOverlaps() { 307 | val table = table { 308 | row { 309 | cell("1") 310 | cell("2\n2") { 311 | rowSpan = 2 312 | } 313 | } 314 | row { 315 | cell("2\n2") { 316 | rowSpan = 2 317 | } 318 | } 319 | row("1") 320 | row("1", "1") 321 | } 322 | 323 | assertEquals( 324 | """ 325 | |12 326 | |22 327 | |21 328 | |11 329 | """.trimMargin(), 330 | table.renderText(), 331 | ) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/DslTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import com.jakewharton.picnic.TextAlignment.BottomCenter 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class DslTest { 8 | @Test fun cellsAppliesStyleToEachCell() { 9 | val table = table { 10 | row { 11 | cells("a", "b\nb", "c\nc\nc") { 12 | alignment = BottomCenter 13 | } 14 | } 15 | } 16 | 17 | assertEquals( 18 | """ 19 | | c 20 | | bc 21 | |abc 22 | """.trimMargin(), 23 | table.renderText(), 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/RepresentativeKotlinDslTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import com.jakewharton.picnic.BorderStyle.Hidden 4 | import com.jakewharton.picnic.TextAlignment.BottomCenter 5 | import com.jakewharton.picnic.TextAlignment.BottomLeft 6 | import com.jakewharton.picnic.TextAlignment.MiddleRight 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class RepresentativeKotlinDslTest { 11 | @Test fun test() { 12 | val table = table { 13 | style { 14 | borderStyle = Hidden 15 | } 16 | cellStyle { 17 | alignment = MiddleRight 18 | paddingLeft = 1 19 | paddingRight = 1 20 | borderLeft = true 21 | borderRight = true 22 | } 23 | header { 24 | cellStyle { 25 | border = true 26 | alignment = BottomLeft 27 | } 28 | row { 29 | cell("APK") { 30 | rowSpan = 2 31 | } 32 | cell("compressed") { 33 | alignment = BottomCenter 34 | columnSpan = 3 35 | } 36 | cell("uncompressed") { 37 | alignment = BottomCenter 38 | columnSpan = 3 39 | } 40 | } 41 | row("old", "new", "diff", "old", "new", "diff") 42 | } 43 | body { 44 | row("dex", "664.8 KiB", "664.8 KiB", "-25 B", "1.5 MiB", "1.5 MiB", "-112 B") 45 | row("arsc", "201.7 KiB", "201.7 KiB", "0 B", "201.6 KiB", "201.6 KiB", "0 B") 46 | row("manifest", "1.4 KiB", "1.4 KiB", "0 B", "4.2 KiB", "4.2 KiB", "0 B") 47 | row("res", "418.2 KiB", "418.2 KiB", "-14 B", "488.3 KiB", "488.3 KiB", "0 B") 48 | row("asset", "0 B", "0 B", "0 B", "0 B", "0 B", "0 B") 49 | row("other", "37.1 KiB", "37.1 KiB", "0 B", "36.3 KiB", "36.3 KiB", "0 B") 50 | } 51 | footer { 52 | cellStyle { 53 | border = true 54 | } 55 | row("total", "1.3 MiB", "1.3 MiB", "-39 B", "2.2 MiB", "2.2 MiB", "-112 B") 56 | } 57 | } 58 | assertEquals( 59 | """ 60 | | │ compressed │ uncompressed 61 | | ├───────────┬───────────┬───────┼───────────┬───────────┬──────── 62 | | APK │ old │ new │ diff │ old │ new │ diff 63 | |──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────── 64 | | dex │ 664.8 KiB │ 664.8 KiB │ -25 B │ 1.5 MiB │ 1.5 MiB │ -112 B 65 | | arsc │ 201.7 KiB │ 201.7 KiB │ 0 B │ 201.6 KiB │ 201.6 KiB │ 0 B 66 | | manifest │ 1.4 KiB │ 1.4 KiB │ 0 B │ 4.2 KiB │ 4.2 KiB │ 0 B 67 | | res │ 418.2 KiB │ 418.2 KiB │ -14 B │ 488.3 KiB │ 488.3 KiB │ 0 B 68 | | asset │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B 69 | | other │ 37.1 KiB │ 37.1 KiB │ 0 B │ 36.3 KiB │ 36.3 KiB │ 0 B 70 | |──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────── 71 | | total │ 1.3 MiB │ 1.3 MiB │ -39 B │ 2.2 MiB │ 2.2 MiB │ -112 B 72 | """.trimMargin(), 73 | table.toString() 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /picnic/src/commonTest/kotlin/com/jakewharton/picnic/TableBorderTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic 2 | 3 | import com.jakewharton.picnic.BorderStyle.Hidden 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class TableBorderTest { 8 | @Test fun tableBorderAndCellBorderMerges() { 9 | val table = table { 10 | style { 11 | border = true 12 | } 13 | cellStyle { 14 | border = true 15 | } 16 | body { 17 | row("A", "B", "C") 18 | row("D", "E", "F") 19 | row("G", "H", "I") 20 | } 21 | } 22 | 23 | assertEquals( 24 | """ 25 | |┌─┬─┬─┐ 26 | |│A│B│C│ 27 | |├─┼─┼─┤ 28 | |│D│E│F│ 29 | |├─┼─┼─┤ 30 | |│G│H│I│ 31 | |└─┴─┴─┘ 32 | """.trimMargin(), 33 | table.renderText(), 34 | ) 35 | } 36 | 37 | @Test fun tableBorderTakesPrecedenceOverCellBorder() { 38 | val table = table { 39 | style { 40 | border = true 41 | } 42 | cellStyle { 43 | border = false 44 | } 45 | body { 46 | row("A", "B", "C") 47 | row("D", "E", "F") 48 | row("G", "H", "I") 49 | } 50 | } 51 | 52 | assertEquals( 53 | """ 54 | |┌───┐ 55 | |│ABC│ 56 | |│DEF│ 57 | |│GHI│ 58 | |└───┘ 59 | """.trimMargin(), 60 | table.renderText(), 61 | ) 62 | } 63 | 64 | @Test fun tableBorderHiddenByBorderStyle() { 65 | val table = table { 66 | style { 67 | border = true 68 | borderStyle = Hidden 69 | } 70 | body { 71 | row("A", "B", "C") 72 | row("D", "E", "F") 73 | row("G", "H", "I") 74 | } 75 | } 76 | 77 | assertEquals( 78 | """ 79 | |ABC 80 | |DEF 81 | |GHI 82 | """.trimMargin(), 83 | table.renderText(), 84 | ) 85 | } 86 | 87 | @Test fun tableBorderWithMiddleBorders() { 88 | val table = table { 89 | style { 90 | border = true 91 | } 92 | header { 93 | cellStyle { 94 | borderBottom = true 95 | } 96 | row { 97 | cell("A") { 98 | borderRight = true 99 | } 100 | cell("B") 101 | cell("C") 102 | } 103 | } 104 | body { 105 | row { 106 | cell("D") { 107 | borderRight = true 108 | } 109 | cell("E") 110 | cell("F") 111 | } 112 | row { 113 | cell("G") { 114 | borderRight = true 115 | } 116 | cell("H") 117 | cell("I") 118 | } 119 | } 120 | footer { 121 | cellStyle { 122 | borderTop = true 123 | } 124 | row { 125 | cell("J") { 126 | borderRight = true 127 | } 128 | cell("K") 129 | cell("L") 130 | } 131 | } 132 | } 133 | 134 | assertEquals( 135 | """ 136 | |┌─┬──┐ 137 | |│A│BC│ 138 | |├─┼──┤ 139 | |│D│EF│ 140 | |│G│HI│ 141 | |├─┼──┤ 142 | |│J│KL│ 143 | |└─┴──┘ 144 | """.trimMargin(), 145 | table.renderText(), 146 | ) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /picnic/src/jvmTest/java/com/jakewharton/picnic/RepresentativeJavaBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.jakewharton.picnic; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static com.jakewharton.picnic.BorderStyle.Hidden; 7 | import static com.jakewharton.picnic.TextAlignment.BottomCenter; 8 | import static com.jakewharton.picnic.TextAlignment.BottomLeft; 9 | import static com.jakewharton.picnic.TextAlignment.MiddleRight; 10 | 11 | public final class RepresentativeJavaBuilderTest { 12 | @Test public void test() { 13 | Table table = new Table.Builder() 14 | .setTableStyle(new TableStyle.Builder() 15 | .setBorderStyle(Hidden) 16 | .build()) 17 | .setCellStyle(new CellStyle.Builder() 18 | .setAlignment(MiddleRight) 19 | .setPaddingLeft(1) 20 | .setPaddingRight(1) 21 | .setBorderLeft(true) 22 | .setBorderRight(true) 23 | .build()) 24 | .setHeader(new TableSection.Builder() 25 | .setCellStyle(new CellStyle.Builder() 26 | .setBorder(true) 27 | .setAlignment(BottomLeft) 28 | .build()) 29 | .addRow(new Row.Builder() 30 | .addCell(new Cell.Builder("APK") 31 | .setRowSpan(2) 32 | .build()) 33 | .addCell(new Cell.Builder("compressed") 34 | .setColumnSpan(3) 35 | .setStyle(new CellStyle.Builder() 36 | .setAlignment(BottomCenter) 37 | .build()) 38 | .build()) 39 | .addCell(new Cell.Builder("uncompressed") 40 | .setColumnSpan(3) 41 | .setStyle(new CellStyle.Builder() 42 | .setAlignment(BottomCenter) 43 | .build()) 44 | .build()) 45 | .build()) 46 | .addRow("old", "new", "diff", "old", "new", "diff") 47 | .build()) 48 | .setBody(new TableSection.Builder() 49 | .addRow("dex", "664.8 KiB", "664.8 KiB", "-25 B", "1.5 MiB", "1.5 MiB", "-112 B") 50 | .addRow("arsc", "201.7 KiB", "201.7 KiB", "0 B", "201.6 KiB", "201.6 KiB", "0 B") 51 | .addRow("manifest", "1.4 KiB", "1.4 KiB", "0 B", "4.2 KiB", "4.2 KiB", "0 B") 52 | .addRow("res", "418.2 KiB", "418.2 KiB", "-14 B", "488.3 KiB", "488.3 KiB", "0 B") 53 | .addRow("asset", "0 B", "0 B", "0 B", "0 B", "0 B", "0 B") 54 | .addRow("other", "37.1 KiB", "37.1 KiB", "0 B", "36.3 KiB", "36.3 KiB", "0 B") 55 | .build()) 56 | .setFooter(new TableSection.Builder() 57 | .setCellStyle(new CellStyle.Builder() 58 | .setBorder(true) 59 | .build()) 60 | .addRow("total", "1.3 MiB", "1.3 MiB", "-39 B", "2.2 MiB", "2.2 MiB", "-112 B") 61 | .build()) 62 | .build(); 63 | assertEquals("" 64 | + " │ compressed │ uncompressed \n" 65 | + " ├───────────┬───────────┬───────┼───────────┬───────────┬────────\n" 66 | + " APK │ old │ new │ diff │ old │ new │ diff \n" 67 | + "──────────┼───────────┼───────────┼───────┼───────────┼───────────┼────────\n" 68 | + " dex │ 664.8 KiB │ 664.8 KiB │ -25 B │ 1.5 MiB │ 1.5 MiB │ -112 B \n" 69 | + " arsc │ 201.7 KiB │ 201.7 KiB │ 0 B │ 201.6 KiB │ 201.6 KiB │ 0 B \n" 70 | + " manifest │ 1.4 KiB │ 1.4 KiB │ 0 B │ 4.2 KiB │ 4.2 KiB │ 0 B \n" 71 | + " res │ 418.2 KiB │ 418.2 KiB │ -14 B │ 488.3 KiB │ 488.3 KiB │ 0 B \n" 72 | + " asset │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B │ 0 B \n" 73 | + " other │ 37.1 KiB │ 37.1 KiB │ 0 B │ 36.3 KiB │ 36.3 KiB │ 0 B \n" 74 | + "──────────┼───────────┼───────────┼───────┼───────────┼───────────┼────────\n" 75 | + " total │ 1.3 MiB │ 1.3 MiB │ -39 B │ 2.2 MiB │ 2.2 MiB │ -112 B ", 76 | table.toString()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample 2 | 3 | Use `./gradlew :sample:installDist` to build and `./sample/build/install/sample/bin/sample` to run. 4 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.jetbrains.kotlin.jvm' 2 | apply plugin: 'application' 3 | 4 | application { 5 | mainClass = 'example.Main' 6 | } 7 | 8 | dependencies { 9 | implementation project(':picnic') 10 | implementation 'com.github.ajalt:mordant:1.2.1' 11 | } 12 | -------------------------------------------------------------------------------- /sample/src/main/java/example/Main.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Main") 2 | 3 | package example 4 | 5 | import com.github.ajalt.mordant.TermColors 6 | import com.jakewharton.picnic.TextAlignment.TopCenter 7 | import com.jakewharton.picnic.TextBorder.Companion.ROUNDED 8 | import com.jakewharton.picnic.renderText 9 | import com.jakewharton.picnic.table 10 | 11 | fun main() { 12 | println() 13 | println( 14 | table { 15 | with(TermColors()) { 16 | header { 17 | cellStyle { 18 | borderBottom = true 19 | } 20 | row { 21 | cell(brightBlue.bg(" Picnic Tables ")) { 22 | alignment = TopCenter 23 | columnSpan = 6 24 | } 25 | } 26 | } 27 | body { 28 | cellStyle { 29 | border = true 30 | paddingLeft = 1 31 | paddingRight = 1 32 | alignment = TopCenter 33 | } 34 | row { 35 | cell("borders${blue("?")}") 36 | cell("padding${red("?")}") 37 | cell("styling${yellow("?")}") 38 | cell("headers/footers${green("?")}") 39 | cell("row/col spans${magenta("?")}") 40 | cell("alignment${cyan("?")}") 41 | } 42 | row { 43 | repeat(6) { 44 | cell(brightGreen("Yes!")) 45 | } 46 | } 47 | } 48 | } 49 | }.renderText(border = ROUNDED), 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':picnic' 2 | include ':sample' 3 | --------------------------------------------------------------------------------