├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── LICENSE-APACHE2 ├── LICENSE-MPL ├── README.md ├── include └── stdout_formatter.hrl ├── mix.exs ├── rebar.config ├── rebar.config.script ├── src ├── stdout_formatter.app.src ├── stdout_formatter.erl ├── stdout_formatter_paragraph.erl ├── stdout_formatter_table.erl └── stdout_formatter_utils.erl └── test ├── main_tests.erl ├── paragraph_tests.erl └── table_tests.erl /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | otp: 14 | - "25.3" 15 | - "26.0" 16 | elixir: 17 | - "1.14" 18 | - "1.15" 19 | steps: 20 | - name: CHECKOUT 21 | uses: actions/checkout@v3 22 | - name: CONFIGURE ERLANG & ELIXIR 23 | uses: erlef/setup-beam@v1 24 | with: 25 | otp-version: ${{ matrix.otp }} 26 | elixir-version: ${{ matrix.elixir }} 27 | - name: MIX COMPILE 28 | run: | 29 | mix compile 30 | - name: REBAR3 eunit 31 | run: | 32 | rebar3 eunit 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /doc/ 3 | /rebar.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim:sw=2:et: 2 | 3 | language: erlang 4 | sudo: false 5 | otp_release: 6 | - 21.3 7 | - 22.3 8 | - 23.0 9 | 10 | install: 11 | - curl -O -L https://s3.amazonaws.com/rebar3/rebar3 12 | - chmod +x rebar3 13 | 14 | script: 15 | - ./rebar3 do compile,eunit,dialyzer 16 | 17 | after_success: 18 | - ./rebar3 coveralls send 19 | 20 | notifications: 21 | email: 22 | on_success: change 23 | on_failure: always 24 | 25 | cache: 26 | directories: 27 | - $HOME/.cache/rebar3/hex/default 28 | - $HOME/_build/default/rebar3_${TRAVIS_OTP_RELEASE}_plt 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This package, stdout_formatter, is dual-licensed under 2 | the Apache License v2 and the Mozilla Public License v2.0. 3 | 4 | For the Apache License, please see the file LICENSE-APACHE2. 5 | 6 | For the Mozilla Public License, please see the file LICENSE-MPL. 7 | 8 | For attribution of copyright and other details of provenance, please 9 | refer to the source code. 10 | 11 | If you have any questions regarding licensing, please contact us at 12 | info@rabbitmq.com. 13 | -------------------------------------------------------------------------------- /LICENSE-APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://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 2017 Pivotal Software Inc. 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 | https://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 | -------------------------------------------------------------------------------- /LICENSE-MPL: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stdout_formatter: Format paragraphs and tables for a human-readable text output 2 | 3 | [![Build Status](https://travis-ci.com/rabbitmq/stdout_formatter.svg?branch=master)](https://travis-ci.com/rabbitmq/stdout_formatter) 4 | [![Coverage Status](https://coveralls.io/repos/github/rabbitmq/stdout_formatter/badge.svg?branch=master)](https://coveralls.io/github/rabbitmq/stdout_formatter) 5 | [![Hex version](https://img.shields.io/hexpm/v/stdout_formatter.svg "Hex version")](https://hex.pm/packages/stdout_formatter) 6 | 7 | **stdout_formatter** is a pure [Erlang application](http://www.erlang.org/) 8 | which allows an application to format paragraphs and tables as text, 9 | usually to display it on a terminal. It only depends on standard 10 | Erlang/OTP applications; no external dependency is required. It doesn't 11 | use native code either (neither port drivers nor NIFs). 12 | 13 | stdout_formatter can be used inside Elixir projects, like any other 14 | Erlang library. You can find an example later in this README. 15 | 16 | stdout_formatter is distributed under the terms of both the **Apache 17 | License v2** and **Mozilla Public Licence v1.1**; see `LICENSE`. 18 | 19 | ## Integrate to your project 20 | 21 | stdout_formatter uses [Rebar 3](http://www.rebar3.org/) as its build 22 | system so it can be integrated to many common build systems. 23 | 24 | ### Rebar 25 | 26 | stdout_formatter is available as a [Hex.pm 27 | package](https://hex.pm/packages/stdout_formatter). Thus you can simply 28 | list it as a package dependency in your `rebar.config`: 29 | 30 | ```erlang 31 | {deps, [stdout_formatter]}. 32 | ``` 33 | 34 | ### Erlang.mk 35 | 36 | Erlang.mk knows about stdout_formatter. You just need to add 37 | `stdout_formatter` as a dependency in your `Makefile`: 38 | 39 | ```make 40 | DEPS = stdout_formatter 41 | dep_stdout_formatter = git https://github.com/rabbitmq/stdout_formatter.git v0.1.0 42 | ``` 43 | 44 | ### Mix 45 | 46 | You can use stdout_formatter in your Elixir 47 | project. stdout_formatter is available as a [Hex.pm 48 | package](https://hex.pm/packages/stdout_formatter). Thus you can simply 49 | list its name in your `mix.exs`: 50 | 51 | ```elixir 52 | def project do 53 | [ 54 | deps: [{:stdout_formatter, "~> 0.1.0"}] 55 | ] 56 | end 57 | ``` 58 | 59 | ## Getting started 60 | 61 | ### Format and display a paragraph of text 62 | 63 | In this example, we want to format a long paragraph of text to fit it in 64 | an 80-columns terminal and display it directly. 65 | 66 | ```erlang 67 | -include_lib("stdout_formatter/include/stdout_formatter.hrl"). 68 | 69 | Text = "Lorem ipsum dolor (...) est laborum.", 70 | 71 | stdout_formatter:display( 72 | #paragraph{ 73 | content = Text 74 | props = #{wrap_at => 72}}). 75 | ``` 76 | ``` 77 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 78 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 79 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 80 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 81 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 82 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 83 | mollit anim id est laborum. 84 | ``` 85 | 86 | ### Format and display a simple table 87 | 88 | In this example, we want to format a table and display it directly. 89 | 90 | ```erlang 91 | -include_lib("stdout_formatter/include/stdout_formatter.hrl"). 92 | 93 | Data = [ 94 | ["Top left", "Top right"], 95 | ["Bottom left", "Bottom right"] 96 | ], 97 | 98 | stdout_formatter:display( 99 | #table{ 100 | rows = Data}). 101 | ``` 102 | ``` 103 | ┌───────────┬────────────┐ 104 | │Top left │Top right │ 105 | ├───────────┼────────────┤ 106 | │Bottom left│Bottom right│ 107 | └───────────┴────────────┘ 108 | ``` 109 | 110 | ### Format and display a complex table with paragraphs inside 111 | 112 | In this example, we mix the previous examples to put a paragraph inside 113 | a table and we start to use colors. 114 | 115 | ```erlang 116 | -include_lib("stdout_formatter/include/stdout_formatter.hrl"). 117 | 118 | Text = "Lorem ipsum dolor (...) est laborum.", 119 | 120 | Data = [ 121 | #row{ 122 | cells = [ 123 | "Initial data", 124 | "Result"], 125 | props = #{title => true}}, 126 | #row{ 127 | cells = [ 128 | "The famous Lorem Ipsum sample", 129 | #paragraph{ 130 | content = Text, 131 | props = #{wrap_at => 40}}]} 132 | ], 133 | 134 | stdout_formatter:display( 135 | #table{ 136 | rows = Data}). 137 | ``` 138 | ``` 139 | ┌─────────────────────────────┬───────────────────────────────────────┐ 140 | │Initial data │Result │ 141 | ├─────────────────────────────┼───────────────────────────────────────┤ 142 | │The famous Lorem Ipsum sample│Lorem ipsum dolor sit amet, consectetur│ 143 | │ │adipiscing elit, sed do eiusmod tempor │ 144 | │ │incididunt ut labore et dolore magna │ 145 | │ │aliqua. Ut enim ad minim veniam, quis │ 146 | │ │nostrud exercitation ullamco laboris │ 147 | │ │nisi ut aliquip ex ea commodo │ 148 | │ │consequat. Duis aute irure dolor in │ 149 | │ │reprehenderit in voluptate velit esse │ 150 | │ │cillum dolore eu fugiat nulla pariatur.│ 151 | │ │Excepteur sint occaecat cupidatat non │ 152 | │ │proident, sunt in culpa qui officia │ 153 | │ │deserunt mollit anim id est laborum. │ 154 | └─────────────────────────────┴───────────────────────────────────────┘ 155 | ``` 156 | 157 | In the example above, what does not appear is the fact the the first row 158 | is using bold text. 159 | -------------------------------------------------------------------------------- /include/stdout_formatter.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -record(formatted_block, {lines = [] :: [stdout_formatter:formatted_line()], 9 | props = #{width => 0, 10 | height => 0} 11 | :: stdout_formatter:formatted_block_props()}). 12 | 13 | -record(formatted_line, {content = "" :: unicode:chardata(), 14 | props = #{width => 0, 15 | reformat_ok => false} 16 | :: stdout_formatter:formatted_line_props()}). 17 | 18 | -record(paragraph, {content :: term(), 19 | props = #{} :: stdout_formatter:paragraph_props()}). 20 | 21 | -record(cell, {content = #formatted_block{} :: stdout_formatter:formattable(), 22 | props = #{} :: stdout_formatter:cell_props()}). 23 | 24 | -record(row, {cells = [] :: [stdout_formatter:formattable() | #cell{}], 25 | props = #{} :: stdout_formatter:row_props()}). 26 | 27 | -record(table, {rows = [] :: [[stdout_formatter:formattable() | #cell{}] | 28 | #row{}], 29 | props = #{} :: stdout_formatter:table_props()}). 30 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule StdoutFormatter.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :stdout_formatter, 7 | description: "Tools to format paragraphs, lists and tables as plain text", 8 | version: "0.2.3", 9 | language: :erlang, 10 | deps: [] 11 | ] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% vim:ft=erlang: 2 | 3 | {cover_enabled, true}. 4 | {cover_print_enabled, true}. 5 | {cover_export_enabled, true}. 6 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %% vim:ft=erlang: 2 | 3 | case os:getenv("TRAVIS_JOB_ID") of 4 | false -> CONFIG; 5 | JobId -> 6 | %% coveralls.io. 7 | [{plugins, [{coveralls, 8 | {git, "https://github.com/markusn/coveralls-erl", 9 | {branch, "master"}}}]} 10 | ,{coveralls_coverdata, "_build/test/cover/eunit.coverdata"} 11 | ,{coveralls_service_name, "travis-ci"} 12 | ,{coveralls_service_job_id, JobId} 13 | |CONFIG 14 | ] 15 | end. 16 | -------------------------------------------------------------------------------- /src/stdout_formatter.app.src: -------------------------------------------------------------------------------- 1 | %% vim:ft=erlang: 2 | 3 | {application, stdout_formatter, 4 | [{description, "Tools to format paragraphs, lists and tables as plain text"}, 5 | {vsn, "0.2.4"}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib]}, 8 | {env, []}, 9 | {modules, []}, 10 | 11 | %% Hex.pm package information. 12 | {licenses, ["Apache 2.0", "MPL 2.0"]}, 13 | {links, [ 14 | {"GitHub", "https://github.com/rabbitmq/stdout_formatter"} 15 | ]}, 16 | {build_tools, ["mix", "rebar3"]}, 17 | {files, ["include", 18 | "LICENSE*", 19 | "mix.exs", 20 | "README.md", 21 | "rebar.config.script", 22 | "rebar.config", 23 | "rebar.lock", 24 | "src"]} 25 | ]}. 26 | -------------------------------------------------------------------------------- /src/stdout_formatter.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | %% @doc 8 | %% This module is the entry point to format structured text. 9 | %% 10 | %% == Which function to use == 11 | %% 12 | %% There are three "levels of formatting": 13 | %%
    14 | %%
  1. {@link format/1} returns an internal structure where the text 15 | %% is already formatted but not ready for display: it is mostly used 16 | %% internally.
  2. 17 | %%
  3. {@link to_string/1} formats the given argument and returns a 18 | %% string ready to be displayed or stored.
  4. 19 | %%
  5. {@link display/1} does the same thing as {@link to_string/1} but 20 | %% displays the result on `stdout' directly (and returns nothing).
  6. 21 | %%
22 | %% 23 | %% == Examples == 24 | %% 25 | %% To automatically wrap a paragraph to fit it in 30 columns and display 26 | %% it: 27 | %% 28 | %% ``` 29 | %% -include_lib("stdout_formatter/include/stdout_formatter.hrl"). 30 | %% 31 | %% stdout_formatter:display( 32 | %% #paragraph{ 33 | %% content = "This module is the entry point to format structured text.", 34 | %% props = #{wrap_at => 30}}). 35 | %% ''' 36 | %% ``` 37 | %% This module is the entry 38 | %% point to format structured 39 | %% text. 40 | %% ''' 41 | %% 42 | %% To format a table and display it: 43 | %% 44 | %% ``` 45 | %% -include_lib("stdout_formatter/include/stdout_formatter.hrl"). 46 | %% 47 | %% stdout_formatter:display( 48 | %% #table{ 49 | %% rows = [["Top left", "Top right"], ["Bottom left", "Bottom right"]], 50 | %% props = #{}}). 51 | %% ''' 52 | %% ``` 53 | %% ┌───────────┬────────────┐ 54 | %% │Top left │Top right │ 55 | %% ├───────────┼────────────┤ 56 | %% │Bottom left│Bottom right│ 57 | %% └───────────┴────────────┘ 58 | %% ''' 59 | 60 | -module(stdout_formatter). 61 | 62 | -include("stdout_formatter.hrl"). 63 | 64 | -export([format/1, 65 | format/2, 66 | to_string/1, 67 | to_string/2, 68 | display/1, 69 | display/2]). 70 | 71 | -type paragraph() :: #paragraph{}. 72 | %% A paragraph of text to format. 73 | %% 74 | %% The content can be a string or any Erlang term. The format string can 75 | %% be specified as a property (See {@link paragraph_props/0}). If it is 76 | %% missing, it will be guessed from the Erlang term. 77 | 78 | -type paragraph_props() :: #{format => string() | none | subterms, 79 | wrap_at => pos_integer() | false, 80 | bold => boolean(), 81 | fg => color() | none, 82 | bg => color() | none, 83 | inherited => map()}. 84 | %% Paragraph properties. 85 | %% 86 | %% The properties are: 87 | %% 94 | 95 | -type table() :: #table{}. 96 | %% A table to format. 97 | %% 98 | %% It is made of a list of rows. Each row is either a {@link row/0} or 99 | %% a list of cells. Each cell is either a {@link cell/0} or a {@link 100 | %% formattable/0}. 101 | 102 | -type table_props() :: #{border_drawing => border_drawing(), 103 | border_style => border_style(), 104 | cell_padding => padding(), 105 | inherited => map()}. 106 | %% Table properties. 107 | %% 108 | %% The properties are: 109 | %% 113 | 114 | -type border_drawing() :: ansi | ascii | none. 115 | %% The line drawing technique. 116 | %% 117 | %% 122 | 123 | -type border_style() :: thin. 124 | %% The style of borders. 125 | 126 | -type padding_value() :: non_neg_integer(). 127 | -type padding() :: padding_value() | 128 | {padding_value(), padding_value()} | 129 | {padding_value(), padding_value(), 130 | padding_value(), padding_value()}. 131 | %% The number of columns/lines of horizontal/vertical padding. 132 | 133 | -type row() :: #row{}. 134 | %% A row in a table. 135 | %% 136 | %% See {@link table/0}. 137 | 138 | -type row_props() :: #{title => boolean(), 139 | title_repeat => pos_integer() | false, 140 | inherited => map()}. 141 | %% Row properties. 142 | %% 143 | %% The properties are: 144 | %% 149 | 150 | -type cell() :: #cell{}. 151 | %% A cell in a table row. 152 | %% 153 | %% See {@link table/0}. 154 | 155 | -type cell_props() :: #{title => boolean(), 156 | padding => padding(), 157 | inherited => map()}. 158 | %% Cell properties. 159 | %% 160 | %% The properties are: 161 | %% 164 | 165 | -type formatted_block() :: #formatted_block{}. 166 | %% A formatted block. 167 | %% 168 | %% It is the result of the {@link format/1} and {@link format/2} 169 | %% functions. It contains a list of {@link formatted_line/0}. 170 | 171 | -type formatted_block_props() :: #{width := non_neg_integer(), 172 | height := non_neg_integer()}. 173 | %% Formatted block properties. 174 | %% 175 | %% The properties are: 176 | %% 180 | 181 | -type formatted_line() :: #formatted_line{}. 182 | %% A formatted line. 183 | 184 | -type formatted_line_props() :: #{width := non_neg_integer(), 185 | reformat_ok := content_if_reformat()}. 186 | %% Formatted line properties. 187 | %% 188 | %% The properties are: 189 | %% 194 | 195 | -type content_if_reformat() :: [{unicode:chardata(), non_neg_integer()} | 196 | {color_start, string(), string()} | 197 | {color_end, string(), string()}] 198 | | derived_from_previous_sibling 199 | | false. 200 | 201 | -type color() :: color_8palette() 202 | | color_256palette() 203 | | true_color(). 204 | %% A color name, index or value. 205 | 206 | -type color_8palette() :: black 207 | | red 208 | | green 209 | | yellow 210 | | blue 211 | | magenta 212 | | cyan 213 | | white 214 | | bright_black 215 | | bright_red 216 | | bright_green 217 | | bright_yellow 218 | | bright_blue 219 | | bright_magenta 220 | | bright_cyan 221 | | bright_white 222 | | 0..15. 223 | %% Color name (atom) or index in the ANSI escape sequence 224 | %% color palette. 225 | 226 | -type color_256palette() :: byte(). 227 | %% Color index in the ANSI 256-color palette. 228 | 229 | -type true_color() :: {Red :: byte(), Green :: byte(), Blue :: byte()}. 230 | %% Three-byte tuple corresponding to RGB 24-bit channel values. 231 | 232 | -type formattable() :: paragraph() | 233 | table() | 234 | formatted_block() | 235 | term(). 236 | 237 | -export_type([formatted_block/0, 238 | formatted_block_props/0, 239 | formatted_line/0, 240 | formatted_line_props/0, 241 | 242 | paragraph/0, 243 | paragraph_props/0, 244 | table/0, 245 | table_props/0, 246 | row/0, 247 | row_props/0, 248 | cell/0, 249 | cell_props/0, 250 | 251 | formattable/0, 252 | color/0, 253 | color_8palette/0, 254 | color_256palette/0, 255 | true_color/0, 256 | content_if_reformat/0]). 257 | 258 | -spec format(formattable()) -> formatted_block(). 259 | %% @doc 260 | %% Formats a term and returns a {@link formatted_block/0}. 261 | %% 262 | %% @param Term Term to format. 263 | %% @returns A {@link formatted_block/0}. 264 | 265 | format(Term) -> 266 | format(Term, #{}). 267 | 268 | -spec format(formattable(), map()) -> formatted_block(). 269 | %% @doc 270 | %% Formats a term and returns a {@link formatted_block/0}. 271 | %% 272 | %% It will use the specified inherited properties. 273 | %% 274 | %% @param Term Term to format. 275 | %% @param InheritedProps Inherited properties map. 276 | %% @returns A {@link formatted_block/0}. 277 | 278 | format(#formatted_block{} = Formatted, _) -> 279 | Formatted; 280 | format(#table{} = Table, InheritedProps) -> 281 | stdout_formatter_table:format(Table, InheritedProps); 282 | format(Term, InheritedProps) -> 283 | stdout_formatter_paragraph:format(Term, InheritedProps). 284 | 285 | -spec to_string(formattable()) -> unicode:chardata(). 286 | %% @doc Formats a term and returns a string. 287 | %% 288 | %% @param Term Term to format as a string. 289 | %% @returns A string of the formatted term. 290 | 291 | to_string(Term) -> 292 | to_string(Term, #{}). 293 | 294 | -spec to_string(formattable(), map()) -> unicode:chardata(). 295 | %% @doc Formats a term and returns a string. 296 | %% 297 | %% It will use the specified inherited properties. 298 | %% 299 | %% @param Term Term to format as a string. 300 | %% @param InheritedProps Inherited properties map. 301 | %% @returns A string of the formatted term. 302 | 303 | to_string(#formatted_block{lines = Lines}, _) -> 304 | lists:flatten( 305 | lists:join( 306 | "\n", 307 | lists:map( 308 | fun(#formatted_line{content = Line}) -> 309 | io_lib:format("~s", [Line]) 310 | end, Lines))); 311 | to_string(Term, InheritedProps) -> 312 | to_string(format(Term, InheritedProps)). 313 | 314 | -spec display(formattable()) -> ok. 315 | %% @doc Formats a term and displays it on `stdout'. 316 | %% 317 | %% @param Term Term to format and display. 318 | 319 | display(Term) -> 320 | display(Term, #{}). 321 | 322 | -spec display(formattable(), map()) -> ok. 323 | %% @doc Formats a term and displays it on `stdout'. 324 | %% 325 | %% It will use the specified inherited properties. 326 | %% 327 | %% @param Term Term to format and display. 328 | %% @param InheritedProps Inherited properties map. 329 | 330 | display(#formatted_block{lines = Lines}, _) -> 331 | lists:foreach( 332 | fun(#formatted_line{content = Line}) -> 333 | io:format("~s~n", [Line]) 334 | end, 335 | Lines); 336 | display(Term, InheritedProps) -> 337 | display(format(Term, InheritedProps)). 338 | -------------------------------------------------------------------------------- /src/stdout_formatter_paragraph.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | %% @doc 8 | %% This module implements the formatting of paragraphs. 9 | 10 | -module(stdout_formatter_paragraph). 11 | 12 | -include("stdout_formatter.hrl"). 13 | 14 | -export([format/1, 15 | format/2, 16 | to_string/1, 17 | to_string/2, 18 | display/1, 19 | display/2]). 20 | 21 | -type paragraph() :: stdout_formatter:paragraph(). 22 | -type paragraph_props() :: stdout_formatter:paragraph_props(). 23 | 24 | -spec format(stdout_formatter:formattable()) -> 25 | stdout_formatter:formatted_block(). 26 | %% @doc 27 | %% Formats a paragraph and returns a {@link formatted_block/0}. 28 | %% 29 | %% @see stdout_formatter:format/1. 30 | 31 | format(Term) -> 32 | format(Term, #{}). 33 | 34 | -spec format(stdout_formatter:formattable(), map()) -> 35 | stdout_formatter:formatted_block(). 36 | %% @doc 37 | %% Formats a paragraph and returns a {@link formatted_block/0}. 38 | %% 39 | %% @see stdout_formatter:format/2. 40 | 41 | format(#paragraph{} = Para, InheritedProps) -> 42 | Para1 = set_default_props(Para, InheritedProps), 43 | do_format(Para1); 44 | format(Term, InheritedProps) -> 45 | format(to_internal_struct(Term), InheritedProps). 46 | 47 | -spec to_string(stdout_formatter:formattable()) -> unicode:chardata(). 48 | %% @doc Formats a paragraph and returns a string. 49 | %% 50 | %% @see stdout_formatter:to_string/1. 51 | 52 | to_string(Para) -> 53 | to_string(Para, #{}). 54 | 55 | -spec to_string(stdout_formatter:formattable(), map()) -> unicode:chardata(). 56 | %% @doc Formats a paragraph and returns a string. 57 | %% 58 | %% @see stdout_formatter:to_string/2. 59 | 60 | to_string(Para, InheritedProps) -> 61 | stdout_formatter:to_string(format(Para, InheritedProps)). 62 | 63 | -spec display(stdout_formatter:formattable()) -> ok. 64 | %% @doc Formats a paragraph and displays it on `stdout'. 65 | %% 66 | %% @see stdout_formatter:display/1. 67 | 68 | display(Para) -> 69 | display(Para, #{}). 70 | 71 | -spec display(stdout_formatter:formattable(), map()) -> ok. 72 | %% @doc Formats a paragraph and displays it on `stdout'. 73 | %% 74 | %% @see stdout_formatter:display/2. 75 | 76 | display(Para, InheritedProps) -> 77 | stdout_formatter:display(format(Para, InheritedProps)). 78 | 79 | -spec to_internal_struct(term()) -> paragraph(). 80 | %% @private 81 | 82 | to_internal_struct(Content) -> 83 | #paragraph{content = Content}. 84 | 85 | -spec set_default_props(paragraph(), map()) -> paragraph(). 86 | %% @private 87 | 88 | set_default_props(#paragraph{} = Para, InheritedProps) -> 89 | #paragraph{props = Props} = Para1 = maybe_set_format_string(Para), 90 | Defaults = #{format => "~p", 91 | wrap_at => false, 92 | bold => false, 93 | fg => none, 94 | bg => none}, 95 | Props1 = stdout_formatter_utils:set_default_props(Props, 96 | Defaults, 97 | InheritedProps), 98 | Props2 = case Props1 of 99 | #{inherited := #{title := true}} -> Props1#{bold => true}; 100 | _ -> Props1 101 | end, 102 | Para1#paragraph{props = Props2}. 103 | 104 | -spec maybe_set_format_string(paragraph()) -> paragraph(). 105 | %% @private 106 | 107 | maybe_set_format_string(#paragraph{props = #{format := _}} = Para) -> 108 | Para; 109 | maybe_set_format_string(#paragraph{content = Content, props = Props} = Para) 110 | when is_atom(Content) -> 111 | Para#paragraph{props = Props#{format => "~s"}}; 112 | maybe_set_format_string(#paragraph{content = Content, props = Props} = Para) 113 | when is_integer(Content) -> 114 | Para#paragraph{props = Props#{format => "~b"}}; 115 | maybe_set_format_string(#paragraph{content = Content, props = Props} = Para) 116 | when is_float(Content) -> 117 | Para#paragraph{props = Props#{format => "~f"}}; 118 | maybe_set_format_string(#paragraph{content = #formatted_block{}, 119 | props = Props} = Para) -> 120 | Para#paragraph{props = Props#{format => subterms}}; 121 | maybe_set_format_string(#paragraph{content = #table{}, 122 | props = Props} = Para) -> 123 | Para#paragraph{props = Props#{format => subterms}}; 124 | maybe_set_format_string(#paragraph{content = Content, props = Props} = Para) 125 | when is_list(Content) -> 126 | try 127 | case unicode:characters_to_list(Content) of 128 | {error, _, _} -> 129 | Para#paragraph{props = Props#{format => "~p"}}; 130 | {incomplete, _, _} -> 131 | Para#paragraph{props = Props#{format => "~p"}}; 132 | Content1 -> 133 | Para#paragraph{content = Content1, 134 | props = Props#{format => none}} 135 | end 136 | catch 137 | _:badarg -> 138 | Para#paragraph{props = Props#{format => subterms}} 139 | end; 140 | maybe_set_format_string(#paragraph{content = Content, props = Props} = Para) 141 | when is_binary(Content) -> 142 | case unicode:characters_to_list(Content) of 143 | {error, _, _} -> 144 | Para#paragraph{props = Props#{format => "~p"}}; 145 | {incomplete, _, _} -> 146 | Para#paragraph{props = Props#{format => "~p"}}; 147 | Content1 -> 148 | Para#paragraph{content = Content1, 149 | props = Props#{format => none}} 150 | end; 151 | maybe_set_format_string(#paragraph{props = Props} = Para) -> 152 | Para#paragraph{props = Props#{format => "~p"}}. 153 | 154 | -spec do_format(paragraph()) -> stdout_formatter:formatted_block(). 155 | %% @private 156 | 157 | do_format(#paragraph{props = #{format := subterms}} = Para) -> 158 | FormattedBlock = format_subterms(Para), 159 | do_format1(Para, FormattedBlock); 160 | do_format(#paragraph{} = Para) -> 161 | FormattedBlock = apply_format_string(Para), 162 | do_format1(Para, FormattedBlock). 163 | 164 | -spec do_format1(paragraph(), stdout_formatter:formatted_block()) -> 165 | stdout_formatter:formatted_block(). 166 | %% @private 167 | 168 | do_format1(#paragraph{props = Props}, 169 | #formatted_block{lines = FormattedLines, 170 | props = BlockProps} = FormattedBlock) -> 171 | FormattedLines1 = wrap_long_lines(FormattedLines, Props), 172 | Width = case FormattedLines of 173 | [] -> 174 | 0; 175 | _ -> 176 | lists:max([LWidth 177 | || #formatted_line{props = #{width := LWidth}} 178 | <- FormattedLines1]) 179 | end, 180 | Height = length(FormattedLines1), 181 | FormattedBlock#formatted_block{lines = FormattedLines1, 182 | props = BlockProps#{width => Width, 183 | height => Height}}. 184 | 185 | -spec apply_format_string(paragraph()) -> stdout_formatter:formatted_block(). 186 | %% @private 187 | 188 | apply_format_string(#paragraph{content = Content, 189 | props = #{format := FormatString} = Props}) -> 190 | String = case FormatString of 191 | none -> Content; 192 | _ -> io_lib:format(FormatString, [Content]) 193 | end, 194 | Lines1 = stdout_formatter_utils:split_lines(String), 195 | Lines2 = [stdout_formatter_utils:expand_tabs(Line) || Line <- Lines1], 196 | {Width, Height} = stdout_formatter_utils:compute_text_block_size(Lines2), 197 | 198 | FormattedLines = [begin 199 | LWidth = stdout_formatter_utils:displayed_length( 200 | Line), 201 | #formatted_line{ 202 | content = Line, 203 | props = #{width => LWidth, 204 | reformat_ok => [{Line, LWidth}]}} 205 | end 206 | || Line <- Lines2], 207 | 208 | FormattedLines1 = apply_colors(FormattedLines, Props), 209 | #formatted_block{lines = FormattedLines1, 210 | props = #{width => Width, 211 | height => Height}}. 212 | 213 | -spec apply_colors([stdout_formatter:formatted_line()], paragraph_props()) -> 214 | [stdout_formatter:formatted_line()]. 215 | %% @private 216 | 217 | apply_colors(Lines, Props) -> 218 | Colors = get_bold_and_colors(Props), 219 | [begin 220 | case Colors of 221 | none -> 222 | Line; 223 | {Start, End} -> 224 | Start1 = lists:flatten(Start), 225 | End1 = lists:flatten(End), 226 | Refmt1 = [{color_start, Start1, End1} | Refmt], 227 | Refmt2 = Refmt1 ++ [{color_end, Start1, End1}], 228 | Line#formatted_line{content = [Start1, Content, End1], 229 | props = LProps#{reformat_ok => Refmt2}} 230 | end 231 | end 232 | || #formatted_line{content = Content, 233 | props = #{reformat_ok := Refmt} = LProps} = Line 234 | <- Lines]. 235 | 236 | -spec get_bold_and_colors(paragraph_props()) -> {string(), string()} | none. 237 | %% @private 238 | 239 | get_bold_and_colors(#{bold := true} = Props) -> 240 | case get_colors(Props) of 241 | {Start, End} -> {["\033[1m", Start], End}; 242 | none -> {"\033[1m", "\033[0m"} 243 | end; 244 | get_bold_and_colors(Props) -> 245 | get_colors(Props). 246 | 247 | -spec get_colors(paragraph_props()) -> 248 | {unicode:chardata(), unicode:chardata()} | none. 249 | %% @private 250 | 251 | get_colors(#{fg := none, bg := none}) -> none; 252 | get_colors(#{fg := Fg, bg := Bg}) -> colors_to_escape_seqs(Fg, Bg). 253 | 254 | -define(is_8color_based(Color), 255 | Color =:= black orelse 256 | Color =:= red orelse 257 | Color =:= green orelse 258 | Color =:= yellow orelse 259 | Color =:= blue orelse 260 | Color =:= magenta orelse 261 | Color =:= cyan orelse 262 | Color =:= white orelse 263 | Color =:= bright_black orelse 264 | Color =:= bright_red orelse 265 | Color =:= bright_green orelse 266 | Color =:= bright_yellow orelse 267 | Color =:= bright_blue orelse 268 | Color =:= bright_magenta orelse 269 | Color =:= bright_cyan orelse 270 | Color =:= bright_white orelse 271 | (Color >= 0 andalso Color =< 15)). 272 | 273 | -spec colors_to_escape_seqs(stdout_formatter:color() | none, 274 | stdout_formatter:color() | none) -> 275 | {unicode:chardata(), unicode:chardata()}. 276 | %% @private 277 | 278 | colors_to_escape_seqs(Fg, none) 279 | when ?is_8color_based(Fg) -> 280 | {["\033[", fg_8color(Fg), "m"], 281 | "\033[0m"}; 282 | colors_to_escape_seqs(none, Bg) 283 | when ?is_8color_based(Bg) -> 284 | {["\033[", bg_8color(Bg), "m"], 285 | "\033[0m"}; 286 | colors_to_escape_seqs(Fg, Bg) 287 | when ?is_8color_based(Fg) andalso ?is_8color_based(Bg) -> 288 | {["\033[", fg_8color(Fg), ";", bg_8color(Bg), "m"], 289 | "\033[0m"}; 290 | colors_to_escape_seqs(Fg, none) -> 291 | {[fg_color(Fg)], 292 | "\033[0m"}; 293 | colors_to_escape_seqs(none, Bg) -> 294 | {[bg_color(Bg)], 295 | "\033[0m"}; 296 | colors_to_escape_seqs(Fg, Bg) -> 297 | {[fg_color(Fg), bg_color(Bg)], 298 | "\033[0m"}. 299 | 300 | -spec fg_8color(stdout_formatter:color_8palette()) -> string(). 301 | %% @private 302 | 303 | fg_8color(Color) when Color =:= black orelse Color =:= 0 -> "30"; 304 | fg_8color(Color) when Color =:= red orelse Color =:= 1 -> "31"; 305 | fg_8color(Color) when Color =:= green orelse Color =:= 2 -> "32"; 306 | fg_8color(Color) when Color =:= yellow orelse Color =:= 3 -> "33"; 307 | fg_8color(Color) when Color =:= blue orelse Color =:= 4 -> "34"; 308 | fg_8color(Color) when Color =:= magenta orelse Color =:= 5 -> "35"; 309 | fg_8color(Color) when Color =:= cyan orelse Color =:= 6 -> "36"; 310 | fg_8color(Color) when Color =:= white orelse Color =:= 7 -> "37"; 311 | 312 | fg_8color(Color) when Color =:= bright_black orelse Color =:= 8 -> "90"; 313 | fg_8color(Color) when Color =:= bright_red orelse Color =:= 9 -> "91"; 314 | fg_8color(Color) when Color =:= bright_green orelse Color =:= 10 -> "92"; 315 | fg_8color(Color) when Color =:= bright_yellow orelse Color =:= 11 -> "93"; 316 | fg_8color(Color) when Color =:= bright_blue orelse Color =:= 12 -> "94"; 317 | fg_8color(Color) when Color =:= bright_magenta orelse Color =:= 13 -> "95"; 318 | fg_8color(Color) when Color =:= bright_cyan orelse Color =:= 14 -> "96"; 319 | fg_8color(Color) when Color =:= bright_white orelse Color =:= 15 -> "97". 320 | 321 | -spec bg_8color(stdout_formatter:color_8palette()) -> string(). 322 | %% @private 323 | 324 | bg_8color(Color) when Color =:= black orelse Color =:= 0 -> "40"; 325 | bg_8color(Color) when Color =:= red orelse Color =:= 1 -> "41"; 326 | bg_8color(Color) when Color =:= green orelse Color =:= 2 -> "42"; 327 | bg_8color(Color) when Color =:= yellow orelse Color =:= 3 -> "43"; 328 | bg_8color(Color) when Color =:= blue orelse Color =:= 4 -> "44"; 329 | bg_8color(Color) when Color =:= magenta orelse Color =:= 5 -> "45"; 330 | bg_8color(Color) when Color =:= cyan orelse Color =:= 6 -> "46"; 331 | bg_8color(Color) when Color =:= white orelse Color =:= 7 -> "47"; 332 | 333 | bg_8color(Color) when Color =:= bright_black orelse Color =:= 8 -> "100"; 334 | bg_8color(Color) when Color =:= bright_red orelse Color =:= 9 -> "101"; 335 | bg_8color(Color) when Color =:= bright_green orelse Color =:= 10 -> "102"; 336 | bg_8color(Color) when Color =:= bright_yellow orelse Color =:= 11 -> "103"; 337 | bg_8color(Color) when Color =:= bright_blue orelse Color =:= 12 -> "104"; 338 | bg_8color(Color) when Color =:= bright_magenta orelse Color =:= 13 -> "105"; 339 | bg_8color(Color) when Color =:= bright_cyan orelse Color =:= 14 -> "106"; 340 | bg_8color(Color) when Color =:= bright_white orelse Color =:= 15 -> "107". 341 | 342 | -define(is_8bit_int(I), I >= 0 andalso I =< 255). 343 | 344 | -spec fg_color(stdout_formatter:color_256palette() | 345 | stdout_formatter:true_color()) -> unicode:chardata(). 346 | %% @private 347 | 348 | fg_color(Index) when ?is_8bit_int(Index) -> 349 | ["\033[38;5;", integer_to_list(Index), "m"]; 350 | fg_color({R, G, B}) 351 | when ?is_8bit_int(R) andalso ?is_8bit_int(G) andalso ?is_8bit_int(B) -> 352 | ["\033[38;2", [[";", integer_to_list(I)] || I <- [R, G, B]], "m"]. 353 | 354 | -spec bg_color(stdout_formatter:color_256palette() | 355 | stdout_formatter:true_color()) -> unicode:chardata(). 356 | %% @private 357 | 358 | bg_color(Index) when ?is_8bit_int(Index) -> 359 | ["\033[48;5;", integer_to_list(Index), "m"]; 360 | bg_color({R, G, B}) 361 | when ?is_8bit_int(R) andalso ?is_8bit_int(G) andalso ?is_8bit_int(B) -> 362 | ["\033[48;2", [[";", integer_to_list(I)] || I <- [R, G, B]], "m"]. 363 | 364 | -spec format_subterms(paragraph()) -> stdout_formatter:formatted_block(). 365 | %% @private 366 | 367 | format_subterms(#paragraph{content = Subterms, props = Props}) -> 368 | InheritedProps = stdout_formatter_utils:merge_inherited_props(Props), 369 | InheritedProps1 = maps:remove(wrap_at, InheritedProps), 370 | FormattedSubterms = case is_list(Subterms) of 371 | true -> 372 | [stdout_formatter:format(Subterm, 373 | InheritedProps1) 374 | || Subterm <- Subterms]; 375 | false -> 376 | [stdout_formatter:format(Subterms, 377 | InheritedProps1)] 378 | end, 379 | concat_formatted_subterms(FormattedSubterms, #formatted_block{}). 380 | 381 | -spec concat_formatted_subterms([stdout_formatter:formatted_block()], 382 | stdout_formatter:formatted_block()) -> 383 | stdout_formatter:formatted_block(). 384 | %% @private 385 | 386 | concat_formatted_subterms( 387 | [FormattedBlock | Rest], 388 | #formatted_block{lines = []}) -> 389 | concat_formatted_subterms(Rest, FormattedBlock); 390 | concat_formatted_subterms( 391 | [#formatted_block{lines = []} | Rest], 392 | Result) -> 393 | concat_formatted_subterms(Rest, Result); 394 | concat_formatted_subterms( 395 | [#formatted_block{lines = [FirstNewLine | NewLines]} | Rest], 396 | #formatted_block{lines = Lines, props = Props} = Result) -> 397 | %% We take the last line of the already-concatenated block, and the 398 | %% first line of the next block to merge them. The width is adjusted 399 | %% to match the new size after concatenation. 400 | [LastLine | RevLines] = lists:reverse(Lines), 401 | #formatted_line{content = LastLineContent, 402 | props = LastLineProps} = LastLine, 403 | #formatted_line{content = FirstNewLineContent, 404 | props = FirstNewLineProps} = FirstNewLine, 405 | #{width := LastLineWidth, 406 | reformat_ok := LastLineRefmt} = LastLineProps, 407 | #{width := FirstNewLineWidth, 408 | reformat_ok := FirstNewLineRefmt} = FirstNewLineProps, 409 | LastLineRefmt1 = case {LastLineRefmt, FirstNewLineRefmt} of 410 | _ when is_list(LastLineRefmt) andalso 411 | is_list(FirstNewLineRefmt) -> 412 | LastLineRefmt ++ FirstNewLineRefmt; 413 | {derived_from_previous_sibling, _} -> 414 | %% A derived line followed by something 415 | %% else: we only remember the "something 416 | %% else" part. 417 | FirstNewLineRefmt; 418 | _ -> 419 | false 420 | end, 421 | LastLine1 = LastLine#formatted_line{ 422 | content = [LastLineContent, FirstNewLineContent], 423 | props = LastLineProps#{ 424 | width => LastLineWidth + FirstNewLineWidth, 425 | reformat_ok => LastLineRefmt1}}, 426 | 427 | NewLines1 = case LastLineRefmt1 of 428 | false -> 429 | %% We also indent the remaining lines of the new 430 | %% block so it keeps its internal alignment. 431 | Padding = lists:duplicate(LastLineWidth, $\s), 432 | [NewLine#formatted_line{ 433 | content = [Padding, NewLineContent], 434 | props = NewLineProps#{ 435 | width => LastLineWidth + NewLineWidth}} 436 | || #formatted_line{content = NewLineContent, 437 | props = #{width := NewLineWidth} = 438 | NewLineProps} = 439 | NewLine <- NewLines]; 440 | _ -> 441 | NewLines 442 | end, 443 | 444 | Lines1 = lists:reverse([LastLine1 | RevLines]) ++ NewLines1, 445 | 446 | %% We put the resulting list of lines back into the block. We also 447 | %% need to adjust the width in the properties. 448 | BlockWidth = lists:max([LWidth 449 | || #formatted_line{props = #{width := LWidth}} 450 | <- Lines1]), 451 | BlockHeight1 = length(Lines1), 452 | Props1 = Props#{width => BlockWidth, 453 | height => BlockHeight1}, 454 | Result1 = Result#formatted_block{lines = Lines1, 455 | props = Props1}, 456 | concat_formatted_subterms(Rest, Result1); 457 | concat_formatted_subterms([], Result) -> 458 | Result. 459 | 460 | -spec wrap_long_lines([stdout_formatter:formatted_line()], 461 | paragraph_props()) -> 462 | [stdout_formatter:formatted_line()]. 463 | %% @private 464 | 465 | wrap_long_lines(FormattedLines, #{wrap_at := false}) -> 466 | FormattedLines; 467 | wrap_long_lines(FormattedLines, #{wrap_at := WrapAt}) 468 | when is_integer(WrapAt) andalso WrapAt > 0 -> 469 | do_wrap_long_lines(FormattedLines, WrapAt, []). 470 | 471 | -spec do_wrap_long_lines([stdout_formatter:formatted_line()], 472 | pos_integer(), 473 | [stdout_formatter:formatted_line()]) -> 474 | [stdout_formatter:formatted_line()]. 475 | %% @private 476 | 477 | do_wrap_long_lines( 478 | [#formatted_line{props = #{reformat_ok := 479 | derived_from_previous_sibling}} | Rest], 480 | WrapAt, 481 | WrappedLines) -> 482 | %% This line will be recomputed from another line, we can drop it. 483 | do_wrap_long_lines(Rest, WrapAt, WrappedLines); 484 | do_wrap_long_lines( 485 | [#formatted_line{props = #{reformat_ok := false}} = Line | Rest], 486 | WrapAt, 487 | WrappedLines) -> 488 | %% This line can't be reformatted, we keep it as is. 489 | do_wrap_long_lines(Rest, WrapAt, WrappedLines ++ [Line]); 490 | do_wrap_long_lines( 491 | [#formatted_line{props = #{width := Width}} = Line | Rest], 492 | WrapAt, 493 | WrappedLines) when Width =< WrapAt -> 494 | %% The line fits into the defined limit, we keep it as is. 495 | do_wrap_long_lines(Rest, WrapAt, WrappedLines ++ [Line]); 496 | do_wrap_long_lines( 497 | [#formatted_line{props = #{reformat_ok := Refmt} = Props} = Line | Rest], 498 | WrapAt, 499 | WrappedLines) when is_list(Refmt) -> 500 | %% This line didn't match the previous conditions, we must reformat 501 | %% it. 502 | [{Content, LWidth} | OtherLines] = wrap_content(Refmt, WrapAt, [], 0, []), 503 | Line1 = Line#formatted_line{content = Content, 504 | props = Props#{width => LWidth}}, 505 | OtherLines1 = [Line#formatted_line{ 506 | content = OtherContent, 507 | props = Props#{width => OtherLWidth, 508 | reformat_ok => 509 | derived_from_previous_sibling}} 510 | || {OtherContent, OtherLWidth} <- OtherLines], 511 | do_wrap_long_lines(Rest, WrapAt, WrappedLines ++ [Line1 | OtherLines1]); 512 | do_wrap_long_lines([], _, WrappedLines) -> 513 | WrappedLines. 514 | 515 | -spec wrap_content(stdout_formatter:content_if_reformat(), 516 | pos_integer(), 517 | stdout_formatter:content_if_reformat(), 518 | non_neg_integer(), 519 | [stdout_formatter:content_if_reformat()]) -> 520 | nonempty_list({unicode:chardata(), non_neg_integer()}). 521 | %% @private 522 | 523 | wrap_content([{Color, _, _} = Content | Rest], WrapAt, 524 | CurrentLine, CurrentWidth, Result) 525 | when Color =:= color_start orelse Color =:= color_end -> 526 | %% A color tag is pushed to the current line as is. It is being 527 | %% handled later on in maybe_reapply_color(). 528 | wrap_content(Rest, WrapAt, [Content | CurrentLine], CurrentWidth, Result); 529 | wrap_content([{_, Width} = Content | Rest], WrapAt, 530 | CurrentLine, CurrentWidth, Result) 531 | when CurrentWidth + Width =< WrapAt -> 532 | %% This chunk fits in the current line (we are still below the 533 | %% WrapAt limit), so push it and move on. 534 | wrap_content(Rest, WrapAt, 535 | [Content | CurrentLine], CurrentWidth + Width, 536 | Result); 537 | wrap_content([{Chunk, Width} = Content | Rest], WrapAt, 538 | CurrentLine, CurrentWidth, Result) -> 539 | %% Take #1: We try to find a space inside the chunk being handled so 540 | %% it fits inside WrapAt in the end. 541 | WrapChunkAt = WrapAt - CurrentWidth, 542 | case wrap_chunk(Chunk, Width, WrapChunkAt) of 543 | nomatch -> 544 | %% Take #2: We look up spaces in previous chunks. 545 | case retry_wrap_content(CurrentLine, [], []) of 546 | nomatch -> 547 | %% Take #3: No space in the current line at all. We 548 | %% cut the chunk in the middle of nowhere to fit 549 | %% into WrapAt. 550 | {LeftPart, RightPart} = wrap_chunk( 551 | Chunk, Width, WrapChunkAt, 552 | force), 553 | 554 | %% The wrap_chunk() above always succeeds, we can 555 | %% finish the current line with the left part and 556 | %% add it to the result, reapplying colors if 557 | %% necessary. 558 | %% 559 | %% The right part is pushed back to the data to 560 | %% handle so the recursion takes care of it. 561 | CurrentLine1 = [LeftPart | CurrentLine], 562 | Rest1 = [RightPart | Rest], 563 | {NewCurrentLine, 564 | NewCurrentWidth, 565 | NewResult} = maybe_reapply_color(CurrentLine1, Result), 566 | wrap_content(Rest1, WrapAt, 567 | NewCurrentLine, NewCurrentWidth, NewResult); 568 | 569 | {LeftChunks, RightChunks} -> 570 | %% We could split one of the previous chunks. The 571 | %% left ones finish the current line which is added 572 | %% to the result. The right ones are pushed back to 573 | %% the data to handle. 574 | Rest1 = RightChunks ++ [Content | Rest], 575 | {NewCurrentLine, 576 | NewCurrentWidth, 577 | NewResult} = maybe_reapply_color(LeftChunks, Result), 578 | wrap_content(Rest1, WrapAt, 579 | NewCurrentLine, NewCurrentWidth, NewResult) 580 | end; 581 | 582 | {LeftPart, RightPart} -> 583 | %% We could split the chunk being handled. Its left part 584 | %% finished the current line which is added to the result. 585 | %% The right part is pushed back to the data to handle. 586 | CurrentLine1 = [LeftPart | CurrentLine], 587 | Rest1 = [RightPart | Rest], 588 | {NewCurrentLine, 589 | NewCurrentWidth, 590 | NewResult} = maybe_reapply_color(CurrentLine1, Result), 591 | wrap_content(Rest1, WrapAt, 592 | NewCurrentLine, NewCurrentWidth, NewResult) 593 | end; 594 | wrap_content([], _, CurrentLine, _, Result) -> 595 | {"", 0, NewResult} = maybe_reapply_color(CurrentLine, Result), 596 | regen_lines(NewResult, []). 597 | 598 | -spec retry_wrap_content(stdout_formatter:content_if_reformat(), 599 | stdout_formatter:content_if_reformat(), 600 | stdout_formatter:content_if_reformat()) -> 601 | nomatch | 602 | {stdout_formatter:content_if_reformat(), 603 | stdout_formatter:content_if_reformat()}. 604 | 605 | retry_wrap_content([{Color, _, _} = Content | Rest], Left, Right) 606 | when Color =:= color_start orelse Color =:= color_end -> 607 | retry_wrap_content(Rest, Left, [Content | Right]); 608 | retry_wrap_content([{Chunk, Width} = Content | Rest], Left, Right) 609 | when is_integer(Width) -> 610 | case wrap_chunk(Chunk, Width, Width) of 611 | nomatch -> 612 | retry_wrap_content(Rest, Left, [Content | Right]); 613 | {LeftPart, RightPart} -> 614 | Left1 = lists:reverse([LeftPart | Left]) ++ Rest, 615 | Right1 = [RightPart | Right], 616 | {Left1, Right1} 617 | end; 618 | retry_wrap_content([], _, _) -> 619 | nomatch. 620 | 621 | -spec wrap_chunk(unicode:chardata(), non_neg_integer(), non_neg_integer()) -> 622 | {{unicode:chardata(), non_neg_integer()}, 623 | {unicode:chardata(), non_neg_integer()}} | 624 | nomatch. 625 | %% @private 626 | 627 | wrap_chunk(Chunk, Width, MaxWidth) -> 628 | wrap_chunk(Chunk, Width, MaxWidth, " "). 629 | 630 | -spec wrap_chunk 631 | (unicode:chardata(), non_neg_integer(), non_neg_integer(), force) -> 632 | {{unicode:chardata(), non_neg_integer()}, 633 | {unicode:chardata(), non_neg_integer()}}; 634 | (unicode:chardata(), non_neg_integer(), non_neg_integer(), unicode:chardata()) -> 635 | {{unicode:chardata(), non_neg_integer()}, 636 | {unicode:chardata(), non_neg_integer()}} | 637 | nomatch. 638 | %% @private 639 | 640 | wrap_chunk(Chunk, _, MaxWidth, force) -> 641 | %% The caller wants to split the string exactly at the specified 642 | %% `MaxWidth' because a previous attempt to split on a whitespace 643 | %% failed. 644 | LeftPart = string:trim(string:slice(Chunk, 0, MaxWidth), trailing), 645 | RightPart = string:trim(string:slice(Chunk, MaxWidth), leading), 646 | {{LeftPart, stdout_formatter_utils:displayed_length(LeftPart)}, 647 | {RightPart, stdout_formatter_utils:displayed_length(RightPart)}}; 648 | wrap_chunk(Chunk, Width, MaxWidth, WrapOn) when Width >= MaxWidth -> 649 | %% The caller wants to split the string so the left part is 650 | %% `MaxWidth' columns at most. Therefore we split it at this mark 651 | %% and look up a space character backward. 652 | LeftPart0 = string:slice(Chunk, 0, MaxWidth), 653 | case string:find(LeftPart0, WrapOn, trailing) of 654 | nomatch -> 655 | nomatch; 656 | RightPart0 -> 657 | %% A space was found and `RightPart0' is the substring 658 | %% starting from it. We have to use that to recover the 659 | %% index of that space character. 660 | Index = MaxWidth - stdout_formatter_utils:displayed_length( 661 | RightPart0), 662 | 663 | %% Now that we have the position of the space character, we 664 | %% can split the original string at that index. 665 | LeftPart = string:trim(string:slice(Chunk, 0, Index), trailing), 666 | RightPart = string:trim(string:slice(Chunk, Index), leading), 667 | {{LeftPart, stdout_formatter_utils:displayed_length(LeftPart)}, 668 | {RightPart, stdout_formatter_utils:displayed_length(RightPart)}} 669 | end. 670 | 671 | -spec maybe_reapply_color(stdout_formatter:content_if_reformat(), 672 | [stdout_formatter:content_if_reformat()]) -> 673 | {stdout_formatter:content_if_reformat(), non_neg_integer(), 674 | [stdout_formatter:content_if_reformat()]}. 675 | %% @private 676 | 677 | maybe_reapply_color(CurrentLine, Result) -> 678 | case find_applied_color(CurrentLine) of 679 | nomatch -> 680 | {[], 0, 681 | [CurrentLine | Result]}; 682 | {color_start, Start, End} -> 683 | {[{color_start, Start, End}], 0, 684 | [[{color_end, Start, End} | CurrentLine] | Result]} 685 | end. 686 | 687 | -spec find_applied_color(stdout_formatter:content_if_reformat()) -> 688 | {color_start, string(), string()} | nomatch. 689 | %% @private 690 | 691 | find_applied_color([{color_start, _, _} = Color | _]) -> 692 | Color; 693 | find_applied_color([{color_end, _, _} | _]) -> 694 | nomatch; 695 | find_applied_color([_ | Rest]) -> 696 | find_applied_color(Rest); 697 | find_applied_color([]) -> 698 | nomatch. 699 | 700 | -spec regen_lines([stdout_formatter:content_if_reformat()], 701 | [{unicode:chardata(), non_neg_integer()}]) -> 702 | [{unicode:chardata(), non_neg_integer()}]. 703 | %% @private 704 | 705 | regen_lines([Parts | Rest], Result) -> 706 | case cleanup_line_and_reverse(Parts) of 707 | [] -> 708 | regen_lines(Rest, Result); 709 | Parts1 -> 710 | Line = regen_line(Parts1, "", 0), 711 | regen_lines(Rest, [Line | Result]) 712 | end; 713 | regen_lines([], Result) -> 714 | Result. 715 | 716 | -spec cleanup_line_and_reverse(stdout_formatter:content_if_reformat()) -> 717 | stdout_formatter:content_if_reformat(). 718 | %% @private 719 | 720 | cleanup_line_and_reverse(Line) -> 721 | Line1 = lists:filter(fun 722 | ({"", 0}) -> false; 723 | (_) -> true 724 | end, Line), 725 | cleanup_line_and_reverse1(Line1, []). 726 | 727 | -spec cleanup_line_and_reverse1(stdout_formatter:content_if_reformat(), 728 | stdout_formatter:content_if_reformat()) -> 729 | stdout_formatter:content_if_reformat(). 730 | %% @private 731 | 732 | cleanup_line_and_reverse1([{color_end, _, _}, {color_start, _, _} | Rest], 733 | Result) -> 734 | cleanup_line_and_reverse1(Rest, Result); 735 | cleanup_line_and_reverse1([Content | Rest], Result) -> 736 | cleanup_line_and_reverse1(Rest, [Content | Result]); 737 | cleanup_line_and_reverse1([], Result) -> 738 | Result. 739 | 740 | -spec regen_line(stdout_formatter:content_if_reformat(), 741 | unicode:chardata(), 742 | non_neg_integer()) -> 743 | {unicode:chardata(), non_neg_integer()}. 744 | %% @private 745 | 746 | regen_line([{color_start, Start, _} | Rest], Line, Width) -> 747 | regen_line(Rest, Line ++ Start, Width); 748 | regen_line([{color_end, _, End} | Rest], Line, Width) -> 749 | regen_line(Rest, Line ++ End, Width); 750 | regen_line([{Chunk, ChunkWidth} | Rest], Line, Width) 751 | when is_integer(ChunkWidth) -> 752 | regen_line(Rest, Line ++ Chunk, Width + ChunkWidth); 753 | regen_line([], Line, Width) -> 754 | {Line, Width}. 755 | -------------------------------------------------------------------------------- /src/stdout_formatter_table.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | %% @doc 8 | %% This module implements the formatting of tables. 9 | 10 | -module(stdout_formatter_table). 11 | 12 | -include("stdout_formatter.hrl"). 13 | 14 | -export([format/1, 15 | format/2, 16 | display/1, 17 | display/2, 18 | to_string/1, 19 | to_string/2]). 20 | 21 | -ifdef(TEST). 22 | -export([set_default_table_props/2, 23 | set_default_row_props/2, 24 | set_default_cell_props/2, 25 | normalize_rows_and_cells/2, 26 | compute_cols_widths/1, 27 | format_cell/1]). 28 | -endif. 29 | 30 | -type table() :: stdout_formatter:table(). 31 | -type row() :: stdout_formatter:row(). 32 | -type cell() :: stdout_formatter:cell(). 33 | 34 | -type normalized_cell() :: #cell{content :: 35 | stdout_formatter:formatted_block()}. 36 | -type normalized_row() :: #row{cells :: [normalized_cell()]}. 37 | 38 | -type cells() :: [stdout_formatter:cell() | stdout_formatter:formattable()]. 39 | -type rows() :: [stdout_formatter:row() | cells()]. 40 | 41 | -type padding_value() :: stdout_formatter:padding_value(). 42 | 43 | -spec format(stdout_formatter:table() | rows()) -> 44 | stdout_formatter:formatted_block(). 45 | %% @doc 46 | %% Formats a table and returns a {@link formatted_block/0}. 47 | %% 48 | %% @see stdout_formatter:format/1. 49 | 50 | format(Table) -> 51 | format(Table, #{}). 52 | 53 | -spec format(stdout_formatter:table() | rows(), map()) -> 54 | stdout_formatter:formatted_block(). 55 | %% @doc 56 | %% Formats a table and returns a {@link formatted_block/0}. 57 | %% 58 | %% @see stdout_formatter:format/2. 59 | 60 | format(#table{rows = []}, _) -> 61 | %% An empty table is formatted as a nul block: no content and a 62 | %% width and height of zero. 63 | #formatted_block{}; 64 | format(#table{rows = Rows} = Table, InheritedProps) -> 65 | %% 1. We set default properties for the entire table. 66 | Table1 = set_default_table_props(Table, InheritedProps), 67 | 68 | %% 2. We normalize rows and cells. At the end, all rows are 69 | %% represented as #row{} records, all cells are represented as a 70 | %% #cell{} records and they contain a #formatted_block{} internal 71 | %% structure (i.e. plain formatted lines of text). Also, if a row 72 | %% has less cells, then we append empty cells. This simplifies 73 | %% what follows because all rows have the same number of columns. 74 | NormalizedRows = normalize_rows_and_cells(Table1, Rows), 75 | 76 | %% 3. We compute the width of each column, thanks to the previous 77 | %% step which took care of formatting the cells' content. 78 | ColsWidths = compute_cols_widths(NormalizedRows), 79 | 80 | %% 4. We format all lines of all rows. Note that a cell may contain 81 | %% multiple lines. Among line, we also have vertical and 82 | %% horizontal borders to separate cells. 83 | {Lines, Width, Height} = format_lines(Table1, NormalizedRows, ColsWidths), 84 | 85 | %% We can return the formatted internal structure. 86 | #formatted_block{lines = Lines, 87 | props = #{width => Width, 88 | height => Height}}; 89 | format(Rows, InheritedProps) when is_list(Rows) -> 90 | format(to_internal_struct(Rows), InheritedProps). 91 | 92 | -spec to_string(stdout_formatter:table() | rows()) -> 93 | unicode:chardata(). 94 | %% @doc 95 | %% Formats a table and returns a string. 96 | %% 97 | %% @see stdout_formatter:to_string/1. 98 | 99 | to_string(Table) -> 100 | to_string(Table, #{}). 101 | 102 | -spec to_string(stdout_formatter:table() | rows(), map()) -> 103 | unicode:chardata(). 104 | %% @doc 105 | %% Formats a table and returns a string. 106 | %% 107 | %% @see stdout_formatter:to_string/2. 108 | 109 | to_string(Table, InheritedProps) -> 110 | stdout_formatter:to_string(format(Table, InheritedProps)). 111 | 112 | -spec display(stdout_formatter:table() | rows()) -> ok. 113 | %% @doc 114 | %% Formats a table and displays it on `stdout'. 115 | %% 116 | %% @see stdout_formatter:display/1. 117 | 118 | display(Table) -> 119 | display(Table, #{}). 120 | 121 | -spec display(stdout_formatter:table() | rows(), map()) -> ok. 122 | %% @doc 123 | %% Formats a table and displays it on `stdout'. 124 | %% 125 | %% @see stdout_formatter:display/2. 126 | 127 | display(Table, InheritedProps) -> 128 | stdout_formatter:display(format(Table, InheritedProps)). 129 | 130 | -spec to_internal_struct(rows()) -> table(). 131 | %% @private 132 | 133 | to_internal_struct([] = Rows) -> 134 | #table{rows = Rows}; 135 | to_internal_struct([_ | _] = Rows) -> 136 | #table{rows = Rows}. 137 | 138 | -spec set_default_table_props(table(), map()) -> table(). 139 | %% @private 140 | 141 | set_default_table_props(#table{props = Props} = Table, InheritedProps) -> 142 | Defaults = #{border_drawing => ansi, 143 | border_style => thin, 144 | cell_padding => 0}, 145 | Props1 = stdout_formatter_utils:set_default_props(Props, 146 | Defaults, 147 | InheritedProps), 148 | Table#table{props = Props1}. 149 | 150 | -spec set_default_row_props(row(), map()) -> row(). 151 | %% @private 152 | 153 | set_default_row_props(#row{props = Props} = Row, InheritedProps) -> 154 | Defaults = #{title => false, 155 | title_repeat => 20}, 156 | Props1 = stdout_formatter_utils:set_default_props(Props, 157 | Defaults, 158 | InheritedProps), 159 | Row#row{props = Props1}. 160 | 161 | -spec set_default_cell_props(cell(), map()) -> cell(). 162 | %% @private 163 | 164 | set_default_cell_props(#cell{props = Props} = Cell, InheritedProps) -> 165 | Padding = case InheritedProps of 166 | #{cell_padding := P} -> P; 167 | _ -> 0 168 | end, 169 | Defaults = #{title => false, 170 | padding => Padding}, 171 | Props1 = stdout_formatter_utils:set_default_props(Props, 172 | Defaults, 173 | InheritedProps), 174 | Cell#cell{props = Props1}. 175 | 176 | -spec normalize_rows_and_cells(table(), rows()) -> [normalized_row()]. 177 | %% @private 178 | 179 | normalize_rows_and_cells(#table{props = TableProps}, Rows) -> 180 | %% The first step is to ensure all rows are represented as # row{} 181 | %% records and cells are represented as #cell{} records and contain 182 | %% already formatted content. 183 | %% 184 | %% While here, we also set default properties for rows if they are 185 | %% missing. 186 | InheritedProps = stdout_formatter_utils:merge_inherited_props(TableProps), 187 | Rows1 = [normalize_row_and_cells(Row, InheritedProps) || Row <- Rows], 188 | 189 | %% The second step is to ensure all rows have the same number of 190 | %% columns. If a row has less, empty cells are appended. 191 | Rows2 = fill_missing_cells(Rows1), 192 | 193 | %% The third step is to compute cell padding when there is an 194 | %% isolated cell which has a specific padding (as opposed to cell 195 | %% padding set globally at the table level. 196 | compute_cells_padding(Rows2). 197 | 198 | -spec normalize_row_and_cells(row() | cells(), map()) -> normalized_row(). 199 | %% @private 200 | 201 | normalize_row_and_cells(Row, InheritedProps) -> 202 | Row1 = any_to_row_record(Row, InheritedProps), 203 | #row{cells = Cells, props = RowProps} = Row1, 204 | InheritedProps1 = stdout_formatter_utils:merge_inherited_props(RowProps), 205 | Cells1 = [normalize_cell(Cell, InheritedProps1) || Cell <- Cells], 206 | Row1#row{cells = Cells1}. 207 | 208 | -spec any_to_row_record(row() | cells(), map()) -> row(). 209 | %% @private 210 | 211 | any_to_row_record(#row{} = Row, InheritedProps) -> 212 | set_default_row_props(Row, InheritedProps); 213 | any_to_row_record(Cells, InheritedProps) when is_list(Cells) -> 214 | any_to_row_record(#row{cells = Cells}, InheritedProps). 215 | 216 | -spec normalize_cell(cell() | stdout_formatter:formattable(), map()) -> 217 | normalized_cell(). 218 | %% @private 219 | 220 | normalize_cell(#cell{} = Cell, InheritedProps) -> 221 | Cell1 = set_default_cell_props(Cell, InheritedProps), 222 | format_cell(Cell1); 223 | normalize_cell(Content, InheritedProps) -> 224 | normalize_cell(#cell{content = Content}, InheritedProps). 225 | 226 | -spec format_cell(cell() | stdout_formatter:formattable()) -> 227 | normalized_cell(). 228 | %% @private 229 | 230 | format_cell(#cell{content = #formatted_block{}} = Cell) -> 231 | Cell; 232 | format_cell(#cell{content = Content, props = Props} = Cell) -> 233 | InheritedProps = stdout_formatter_utils:merge_inherited_props(Props), 234 | Content1 = stdout_formatter:format(Content, InheritedProps), 235 | Cell#cell{content = Content1}; 236 | format_cell(Content) -> 237 | format_cell(#cell{content = Content}). 238 | 239 | -spec fill_missing_cells([normalized_row()]) -> [normalized_row()]. 240 | %% @private 241 | 242 | fill_missing_cells([]) -> 243 | []; 244 | fill_missing_cells(Rows) -> 245 | MaxCellsPerRow = lists:max([length(Cells) || #row{cells = Cells} <- Rows]), 246 | #row{props = RowProps} = hd(Rows), 247 | InheritedProps = stdout_formatter_utils:merge_inherited_props(RowProps), 248 | EmptyCell = set_default_cell_props( 249 | #cell{content = #formatted_block{}}, 250 | InheritedProps), 251 | [begin 252 | CellsCount = length(Cells), 253 | case CellsCount < MaxCellsPerRow of 254 | true -> 255 | Cells1 = lists:append( 256 | Cells, 257 | lists:duplicate(MaxCellsPerRow - CellsCount, EmptyCell)), 258 | Row#row{cells = Cells1}; 259 | false -> 260 | Row 261 | end 262 | end 263 | || #row{cells = Cells} = Row <- Rows]. 264 | 265 | -spec compute_cells_padding([normalized_row()]) -> [normalized_row()]. 266 | %% @private 267 | 268 | compute_cells_padding([#row{cells = Cells} | _] = Rows) -> 269 | HorizontalPadding = compute_cells_horizontal_padding( 270 | Rows, 271 | lists:duplicate(length(Cells), {0, 0})), 272 | compute_cells_padding1(Rows, HorizontalPadding, []); 273 | compute_cells_padding([] = Rows) -> 274 | Rows. 275 | 276 | -spec compute_cells_padding1( 277 | [normalized_row()], 278 | [{padding_value(), padding_value()}], 279 | [normalized_row()]) -> 280 | [normalized_row()]. 281 | %% @private 282 | 283 | compute_cells_padding1([#row{cells = Cells} = Row | Rest], 284 | HorizontalPadding, 285 | Result) -> 286 | VerticalPadding = compute_cells_vertical_padding(Cells, 0, 0), 287 | Cells1 = compute_cells_padding2(Cells, 288 | VerticalPadding, 289 | HorizontalPadding, 290 | []), 291 | Row1 = Row#row{cells = Cells1}, 292 | compute_cells_padding1(Rest, HorizontalPadding, [Row1 | Result]); 293 | compute_cells_padding1([], _, Result) -> 294 | lists:reverse(Result). 295 | 296 | -spec compute_cells_padding2( 297 | [normalized_cell()], 298 | {padding_value(), padding_value()}, 299 | [{padding_value(), padding_value()}], 300 | [normalized_cell()]) -> 301 | [normalized_cell()]. 302 | %% @private 303 | 304 | compute_cells_padding2([#cell{props = Props} = Cell | Rest1], 305 | {Top, Bottom} = VerticalPadding, 306 | [{Left, Right} | Rest2], 307 | Result) -> 308 | Props1 = Props#{padding => {Top, Right, Bottom, Left}}, 309 | Cell1 = Cell#cell{props = Props1}, 310 | compute_cells_padding2(Rest1, VerticalPadding, Rest2, [Cell1 | Result]); 311 | compute_cells_padding2([], _, [], Result) -> 312 | lists:reverse(Result). 313 | 314 | -spec compute_cells_horizontal_padding( 315 | [normalized_row()], 316 | [{padding_value(), padding_value()}]) -> 317 | [{padding_value(), padding_value()}]. 318 | %% @private 319 | 320 | compute_cells_horizontal_padding([#row{cells = Cells} | Rest], 321 | HorizontalPadding) -> 322 | HorizontalPadding1 = compute_cells_horizontal_padding1( 323 | Cells, 324 | HorizontalPadding, 325 | []), 326 | compute_cells_horizontal_padding(Rest, HorizontalPadding1); 327 | compute_cells_horizontal_padding([], HorizontalPadding) -> 328 | HorizontalPadding. 329 | 330 | -spec compute_cells_horizontal_padding1( 331 | [normalized_cell()], 332 | [{padding_value(), padding_value()}], 333 | [{padding_value(), padding_value()}]) -> 334 | [{padding_value(), padding_value()}]. 335 | %% @private 336 | 337 | compute_cells_horizontal_padding1([Cell | Rest1], 338 | [{MaxLeft, MaxRight} | Rest2], 339 | Result) -> 340 | Left = get_left_padding(Cell), 341 | Right = get_right_padding(Cell), 342 | MaxLeft1 = erlang:max(Left, MaxLeft), 343 | MaxRight1 = erlang:max(Right, MaxRight), 344 | compute_cells_horizontal_padding1(Rest1, 345 | Rest2, 346 | [{MaxLeft1, MaxRight1} | Result]); 347 | compute_cells_horizontal_padding1([], [], Result) -> 348 | lists:reverse(Result). 349 | 350 | -spec compute_cells_vertical_padding( 351 | [normalized_cell()], 352 | padding_value(), 353 | padding_value()) -> 354 | {padding_value(), padding_value()}. 355 | %% @private 356 | 357 | compute_cells_vertical_padding([Cell | Rest], MaxTop, MaxBottom) -> 358 | Top = get_top_padding(Cell), 359 | Bottom = get_bottom_padding(Cell), 360 | MaxTop1 = erlang:max(Top, MaxTop), 361 | MaxBottom1 = erlang:max(Bottom, MaxBottom), 362 | compute_cells_vertical_padding(Rest, MaxTop1, MaxBottom1); 363 | compute_cells_vertical_padding([], MaxTop, MaxBottom) -> 364 | {MaxTop, MaxBottom}. 365 | 366 | -spec compute_cols_widths([normalized_row()]) -> [non_neg_integer()]. 367 | %% @private 368 | 369 | compute_cols_widths([]) -> 370 | []; 371 | compute_cols_widths([#row{cells = Cells} | _] = Rows) -> 372 | ColsCount = length(Cells), 373 | ColsWidths = lists:duplicate(ColsCount, 0), 374 | do_compute_cols_widths(Rows, ColsWidths). 375 | 376 | -spec do_compute_cols_widths([normalized_row()], [non_neg_integer()]) -> 377 | [non_neg_integer()]. 378 | %% @private 379 | 380 | do_compute_cols_widths([#row{cells = Cells} | Rest], ColsWidths) -> 381 | ColsWidths1 = lists:zipwith( 382 | fun(#cell{content = 383 | #formatted_block{ 384 | props = #{width := CurrentWidth}}} = Cell, 385 | MaxWidth) -> 386 | LeftPadding = get_left_padding(Cell), 387 | RightPadding = get_right_padding(Cell), 388 | CurrentWidth1 = 389 | CurrentWidth + LeftPadding + RightPadding, 390 | erlang:max(CurrentWidth1, MaxWidth) 391 | end, Cells, ColsWidths), 392 | do_compute_cols_widths(Rest, ColsWidths1); 393 | do_compute_cols_widths([], ColsWidths) -> 394 | ColsWidths. 395 | 396 | -spec get_top_padding(normalized_cell()) -> padding_value(). 397 | %% @private 398 | 399 | get_top_padding(#cell{props = #{padding := {Padding, _, _, _}}}) -> 400 | Padding; 401 | get_top_padding(#cell{props = #{padding := {Padding, _}}}) -> 402 | Padding; 403 | get_top_padding(#cell{props = #{padding := Padding}}) -> 404 | Padding. 405 | 406 | -spec get_right_padding(normalized_cell()) -> padding_value(). 407 | %% @private 408 | 409 | get_right_padding(#cell{props = #{padding := {_, Padding, _, _}}}) -> 410 | Padding; 411 | get_right_padding(#cell{props = #{padding := {_, Padding}}}) -> 412 | Padding; 413 | get_right_padding(#cell{props = #{padding := Padding}}) -> 414 | Padding. 415 | 416 | -spec get_bottom_padding(normalized_cell()) -> padding_value(). 417 | %% @private 418 | 419 | get_bottom_padding(#cell{props = #{padding := {_, _, Padding, _}}}) -> 420 | Padding; 421 | get_bottom_padding(#cell{props = #{padding := {Padding, _}}}) -> 422 | Padding; 423 | get_bottom_padding(#cell{props = #{padding := Padding}}) -> 424 | Padding. 425 | 426 | -spec get_left_padding(normalized_cell()) -> padding_value(). 427 | %% @private 428 | 429 | get_left_padding(#cell{props = #{padding := {_, _, _, Padding}}}) -> 430 | Padding; 431 | get_left_padding(#cell{props = #{padding := {_, Padding}}}) -> 432 | Padding; 433 | get_left_padding(#cell{props = #{padding := Padding}}) -> 434 | Padding. 435 | 436 | -spec format_lines(table(), [normalized_row()], [non_neg_integer()]) -> 437 | {[stdout_formatter:formatted_line()], 438 | non_neg_integer(), 439 | non_neg_integer()}. 440 | %% @private 441 | 442 | format_lines(Table, Rows, ColsWidths) -> 443 | format_lines(Table, Rows, ColsWidths, [], 0, 0). 444 | 445 | format_lines(Table, 446 | [Row | Rest], 447 | ColsWidths, 448 | FinalLines, 449 | _FinalWidth, 450 | FinalHeight) -> 451 | %% When we format a single row, we take care of the horizontal 452 | %% border just above it. 453 | %% 454 | %% Based on the number of final lines already produced, we determine 455 | %% if that horizontal border is the top of the table or somewhere in 456 | %% the middle. 457 | {BorderLines, BorderWidth, BorderHeight} = draw_row_horizontal_border( 458 | Table, 459 | ColsWidths, 460 | case FinalLines of 461 | [] -> top; 462 | _ -> inside 463 | end), 464 | 465 | %% We add cell padding: this step takes care of adding spaces before 466 | %% and/or after each line of the cells' content. This ensures that: 467 | %% 1. Content is aligned as expected (right/center/left). 468 | %% 2. All lines are normalized and have now the same width 469 | %% (including trailing spaces). 470 | Row1 = add_cell_padding(Table, Row, ColsWidths), 471 | 472 | %% We transform cells into lines. E.g. line #1 of cell #1 and line 473 | %% #2 of cell #2 are concatenated, with vertical borders also added 474 | %% if needed. Then we do the same with line #2 of cell #1 and line 475 | %% #2 of cell #2, and so on. 476 | {RowLines, _Width, Height} = format_lines1(Table, Row1), 477 | 478 | format_lines(Table, 479 | Rest, 480 | ColsWidths, 481 | FinalLines ++ BorderLines ++ RowLines, 482 | BorderWidth, 483 | FinalHeight + BorderHeight + Height); 484 | format_lines(Table, 485 | [], 486 | ColsWidths, 487 | FinalLines, 488 | FinalWidth, 489 | FinalHeight) -> 490 | %% We are done with all rows: we can prepare the bottom horizontal 491 | %% border. 492 | {BorderLines, _BorderWidth, BorderHeight} = draw_row_horizontal_border( 493 | Table, 494 | ColsWidths, 495 | bottom), 496 | {FinalLines ++ BorderLines, 497 | FinalWidth, 498 | FinalHeight + BorderHeight}. 499 | 500 | -spec format_lines1(table(), normalized_row()) -> 501 | {[stdout_formatter:formatted_line()], 502 | non_neg_integer(), 503 | non_neg_integer()}. 504 | %% @private 505 | 506 | format_lines1(Table, 507 | #row{cells = 508 | [#cell{content = 509 | #formatted_block{ 510 | props = #{height := CellHeight}}} | _] = Cells 511 | } = Row) -> 512 | {BorderLines, BorderWidth, BorderHeight} = draw_row_vertical_border( 513 | Table, CellHeight), 514 | format_lines2(Table, Row, Cells, BorderLines, BorderWidth, BorderHeight). 515 | 516 | -spec format_lines2(table(), normalized_row(), [normalized_cell()], 517 | [stdout_formatter:formatted_line()], 518 | non_neg_integer(), non_neg_integer()) -> 519 | {[stdout_formatter:formatted_line()], 520 | non_neg_integer(), 521 | non_neg_integer()}. 522 | %% @private 523 | 524 | format_lines2(Table, 525 | Row, 526 | [#cell{content = 527 | #formatted_block{ 528 | lines = CellLines, 529 | props = #{width := CellWidth, 530 | height := CellHeight}}} | Rest], 531 | FinalLines, 532 | FinalWidth, 533 | FinalHeight) -> 534 | {BorderLines, BorderWidth, BorderHeight} = draw_row_vertical_border( 535 | Table, CellHeight), 536 | FinalLines1 = lists:zipwith3( 537 | fun(#formatted_line{content = FinalContent, 538 | props = FinalProps} = FinalLine, 539 | #formatted_line{content = CellContent, 540 | props = CellProps}, 541 | #formatted_line{content = BorderContent, 542 | props = BorderProps}) -> 543 | #{width := FinalLWidth} = FinalProps, 544 | #{width := CellLWidth} = CellProps, 545 | #{width := BorderLWidth} = BorderProps, 546 | LWidth = FinalLWidth + CellLWidth + BorderLWidth, 547 | FinalLine#formatted_line{ 548 | content = [FinalContent, 549 | CellContent, 550 | BorderContent], 551 | props = FinalProps#{width => LWidth} 552 | } 553 | end, 554 | FinalLines, CellLines, BorderLines), 555 | FinalWidth1 = FinalWidth + CellWidth + BorderWidth, 556 | FinalHeight = BorderHeight, 557 | format_lines2(Table, Row, Rest, FinalLines1, FinalWidth1, FinalHeight); 558 | format_lines2(_, _, [], FinalLines, FinalWidth, FinalHeight) -> 559 | {FinalLines, FinalWidth, FinalHeight}. 560 | 561 | -spec add_cell_padding(table(), normalized_row(), [non_neg_integer()]) -> 562 | normalized_row(). 563 | %% @private 564 | 565 | add_cell_padding(Table, #row{cells = Cells} = Row, ColsWidths) -> 566 | add_cell_horizontal_padding(Table, Row, Cells, ColsWidths, 0, []). 567 | 568 | -spec add_cell_horizontal_padding(table(), 569 | normalized_row(), 570 | [normalized_cell()], 571 | [non_neg_integer()], 572 | non_neg_integer(), 573 | [normalized_cell()]) -> 574 | normalized_row(). 575 | %% @private 576 | 577 | add_cell_horizontal_padding( 578 | Table, 579 | Row, 580 | [#cell{content = 581 | #formatted_block{lines = Lines, 582 | props = #{height := CellHeight} = Props} = Content 583 | } = Cell | Rest1], 584 | [ColWidth | Rest2], 585 | MaxRowHeight, 586 | PaddedCells) -> 587 | LeftPaddingW = get_left_padding(Cell), 588 | LeftPadding = lists:duplicate(LeftPaddingW, $\s), 589 | PaddedLines = [begin 590 | #{width := LWidth} = LProps, 591 | Padding = ColWidth - LWidth - LeftPaddingW, 592 | Line#formatted_line{ 593 | content = [LeftPadding, 594 | LContent, 595 | lists:duplicate(Padding, $\s)], 596 | props = LProps#{width => 597 | LWidth + 598 | Padding + 599 | LeftPaddingW}} 600 | end 601 | || #formatted_line{content = LContent, 602 | props = LProps} = Line <- Lines], 603 | Props1 = Props#{width => ColWidth}, 604 | PaddedCell = Cell#cell{ 605 | content = Content#formatted_block{lines = PaddedLines, 606 | props = Props1}}, 607 | MaxRowHeight1 = erlang:max(CellHeight, MaxRowHeight), 608 | add_cell_horizontal_padding(Table, 609 | Row, 610 | Rest1, 611 | Rest2, 612 | MaxRowHeight1, 613 | [PaddedCell | PaddedCells]); 614 | add_cell_horizontal_padding(Table, Row, [], [], MaxRowHeight, PaddedCells) -> 615 | add_cell_vertical_padding(Table, Row, PaddedCells, MaxRowHeight, []). 616 | 617 | -spec add_cell_vertical_padding(table(), 618 | normalized_row(), 619 | [normalized_cell()], 620 | non_neg_integer(), 621 | [normalized_cell()]) -> 622 | normalized_row(). 623 | %% @private 624 | 625 | add_cell_vertical_padding( 626 | Table, 627 | Row, 628 | [#cell{content = 629 | #formatted_block{lines = Lines, 630 | props = #{width := CellWidth, 631 | height := CellHeight} = Props} = Content 632 | } = Cell | Rest], 633 | RowHeight, 634 | PaddedCells) -> 635 | TopPaddingH = get_top_padding(Cell), 636 | BottomPaddingH = get_bottom_padding(Cell), 637 | RowHeight1 = RowHeight + TopPaddingH + BottomPaddingH, 638 | PaddedCell = case CellHeight < RowHeight1 of 639 | true -> 640 | EmptyContent = lists:duplicate(CellWidth, $\s), 641 | EmptyProps = #{width => CellWidth, 642 | reformat_ok => false}, 643 | EmptyLine = #formatted_line{ 644 | content = EmptyContent, 645 | props = EmptyProps}, 646 | PaddedLines = lists:duplicate(TopPaddingH, 647 | EmptyLine) ++ 648 | Lines ++ 649 | lists:duplicate(RowHeight 650 | - CellHeight 651 | + BottomPaddingH, 652 | EmptyLine), 653 | Props1 = Props#{height => RowHeight1}, 654 | Cell#cell{ 655 | content = 656 | Content#formatted_block{lines = PaddedLines, 657 | props = Props1}}; 658 | false -> 659 | Cell 660 | end, 661 | add_cell_vertical_padding(Table, 662 | Row, 663 | Rest, 664 | RowHeight, 665 | [PaddedCell | PaddedCells]); 666 | add_cell_vertical_padding(_, Row, [], _, PaddedCells) -> 667 | Row#row{cells = PaddedCells}. 668 | 669 | -define(border_drawing_is_valid(V), 670 | V =:= ansi orelse 671 | V =:= ascii). 672 | -define(border_style_is_valid(V), 673 | V =:= thin orelse 674 | V =:= none). 675 | 676 | -spec draw_row_horizontal_border(table(), [non_neg_integer()], 677 | top | inside | bottom) -> 678 | {[stdout_formatter:formatted_line()], 679 | non_neg_integer(), 680 | non_neg_integer()}. 681 | %% @private 682 | 683 | draw_row_horizontal_border(#table{props = #{border_drawing := none}}, _, _) -> 684 | { 685 | [], 686 | 0, 687 | 0 688 | }; 689 | draw_row_horizontal_border(#table{props = #{border_drawing := Drawing, 690 | border_style := Style}}, 691 | ColsWidths, 692 | Where) 693 | when ?border_drawing_is_valid(Drawing) andalso 694 | ?border_style_is_valid(Style) -> 695 | {StartChar, MiddleChar, EndChar, FillingChar} = 696 | case Drawing of 697 | ascii -> 698 | {"+", "+", "+", "-"}; 699 | ansi -> 700 | case Style of 701 | thin -> 702 | case Where of 703 | top -> {"\033(0l", "w", "k\033(B", "q"}; 704 | inside -> {"\033(0t", "n", "u\033(B", "q"}; 705 | bottom -> {"\033(0m", "v", "j\033(B", "q"} 706 | end 707 | end 708 | end, 709 | Line = lists:flatten( 710 | [StartChar, 711 | lists:duplicate(hd(ColsWidths), FillingChar), 712 | [[MiddleChar, lists:duplicate(ColWidth, FillingChar)] 713 | || ColWidth <- tl(ColsWidths)], 714 | EndChar]), 715 | Width = lists:sum(ColsWidths) + length(ColsWidths) + 1, 716 | Height = 1, 717 | { 718 | [ 719 | #formatted_line{ 720 | content = Line, 721 | props = #{width => Width, 722 | reformat_ok => false} 723 | } 724 | ], 725 | Width, 726 | Height 727 | }. 728 | 729 | -spec draw_row_vertical_border(table(), pos_integer()) -> 730 | {[stdout_formatter:formatted_line()], 731 | non_neg_integer(), 732 | non_neg_integer()}. 733 | %% @private 734 | 735 | draw_row_vertical_border(#table{props = #{border_drawing := none}}, 736 | Height) -> 737 | { 738 | lists:duplicate(Height, #formatted_line{}), 739 | 0, 740 | Height 741 | }; 742 | draw_row_vertical_border(#table{props = #{border_drawing := Drawing, 743 | border_style := Style}}, 744 | Height) 745 | when ?border_drawing_is_valid(Drawing) andalso 746 | ?border_style_is_valid(Style) -> 747 | LineContent = case Drawing of 748 | ascii -> 749 | "|"; 750 | ansi -> 751 | case Style of 752 | thin -> "\033(0x\033(B" 753 | end 754 | end, 755 | Width = 1, 756 | Line = #formatted_line{content = LineContent, 757 | props = #{width => Width, 758 | reformat_ok => false}}, 759 | { 760 | lists:duplicate(Height, Line), 761 | Width, 762 | Height 763 | }. 764 | -------------------------------------------------------------------------------- /src/stdout_formatter_utils.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | %% @doc 8 | %% This module contains utility functions used by other modules in this 9 | %% library. 10 | 11 | -module(stdout_formatter_utils). 12 | 13 | -export([set_default_props/3, 14 | merge_inherited_props/1, 15 | split_lines/1, 16 | expand_tabs/1, 17 | compute_text_block_size/1, 18 | displayed_length/1, 19 | remove_escape_sequences/1]). 20 | 21 | -define(TAB_SIZE, 8). 22 | 23 | -spec set_default_props(map(), map(), map()) -> map(). 24 | %% @doc 25 | %% Fills the initial properties map with default and inherited values. 26 | %% 27 | %% For each property, the value comes from the first source which 28 | %% provides one, with the following order of precedence: 29 | %%
    30 | %%
  1. the initial properties map
  2. 31 | %%
  3. the inherited properties
  4. 32 | %%
  5. the default properties
  6. 33 | %%
34 | %% 35 | %% In the end, the final properties map contains exactly the same keys 36 | %% as those in the default properties map, plus the `inherited' key 37 | %% which contains the inherited properties map. 38 | %% 39 | %% @param Props Initial properties map. 40 | %% @param Defaults Default properties map. 41 | %% @param InheritedProps Inherited properties map. 42 | %% @returns the completed properties map. 43 | 44 | set_default_props(Props, Defaults, InheritedProps) -> 45 | Props1 = maps:merge(InheritedProps, Props), 46 | Props2 = maps:merge(Defaults, Props1), 47 | Props3 = Props2#{inherited => InheritedProps}, 48 | Props4 = maps:filter( 49 | fun 50 | (inherited, _) -> true; 51 | (Key, _) -> maps:is_key(Key, Defaults) 52 | end, 53 | Props3), 54 | Props4. 55 | 56 | -spec merge_inherited_props(map()) -> map(). 57 | %% @doc 58 | %% Prepares the properties map to pass to the child terms. 59 | %% 60 | %% The current term's properties are merged with the inherited 61 | %% properties map it initially received (as stored in the `inherited' 62 | %% key), the current term's properties having precedence. 63 | %% 64 | %% The result can be used to pass to child terms' formatting function. 65 | %% 66 | %% @param Props Current term's properties map. 67 | %% @returns the computed inherited properties map to pass to child terms. 68 | 69 | merge_inherited_props(#{inherited := InheritedProps} = Props) -> 70 | maps:merge(maps:remove(inherited, InheritedProps), 71 | maps:remove(inherited, Props)); 72 | merge_inherited_props(Props) -> 73 | Props. 74 | 75 | -spec split_lines(unicode:chardata()) -> [unicode:chardata()]. 76 | %% @doc 77 | %% Splits a text into a list of lines. 78 | %% 79 | %% @param Text Text to split into lines. 80 | %% @returns the list of lines. 81 | 82 | split_lines(Text) -> 83 | [string:trim(Line, trailing, [$\r,$\n]) || 84 | Line <- string:split(Text, "\n", all)]. 85 | 86 | -spec expand_tabs(unicode:chardata()) -> unicode:chardata(). 87 | %% @doc 88 | %% Expands tab characters to spaces. 89 | %% 90 | %% Tab characters are replaced by spaces so that the content is aligned 91 | %% on 8-column boundaries. 92 | %% 93 | %% @param Line Line of text to expand. 94 | %% @returns the same line with tab characters expanded. 95 | 96 | expand_tabs(Line) -> 97 | Parts = string:split(Line, "\t", all), 98 | do_expand_tabs(Parts, []). 99 | 100 | -spec do_expand_tabs([unicode:chardata()], unicode:chardata()) -> 101 | unicode:chardata(). 102 | %% @private 103 | 104 | do_expand_tabs([Part], ExpandedParts) -> 105 | lists:reverse([Part | ExpandedParts]); 106 | do_expand_tabs([Part | Rest], ExpandedParts) -> 107 | Width = displayed_length(Part), 108 | Padding = ?TAB_SIZE - (Width rem 8), 109 | ExpandedPart = [Part, lists:duplicate(Padding, $\s)], 110 | do_expand_tabs(Rest, [ExpandedPart | ExpandedParts]). 111 | 112 | -spec compute_text_block_size([unicode:chardata()]) -> 113 | {ColumnsCount :: non_neg_integer(), RowsCount :: non_neg_integer()}. 114 | %% @doc 115 | %% Computes the size of the rectangle which contains the given list of 116 | %% lines. 117 | %% 118 | %% The size is returned as the number of columns and rows. 119 | %% 120 | %% @param Lines List of text lines. 121 | %% @returns the columns and rows count of the rectangle containing the 122 | %% lines. 123 | 124 | compute_text_block_size(Lines) when is_list(Lines) -> 125 | compute_text_block_size(Lines, 0, 0). 126 | 127 | -spec compute_text_block_size([unicode:chardata()], 128 | non_neg_integer(), 129 | non_neg_integer()) -> 130 | {ColumnsCount :: non_neg_integer(), RowsCount :: non_neg_integer()}. 131 | %% @private 132 | 133 | compute_text_block_size([Line | Rest], MaxWidth, MaxHeight) -> 134 | Width = displayed_length(Line), 135 | NewMaxWidth = case Width > MaxWidth of 136 | true -> Width; 137 | false -> MaxWidth 138 | end, 139 | compute_text_block_size(Rest, NewMaxWidth, MaxHeight + 1); 140 | compute_text_block_size([], MaxWidth, MaxHeight) -> 141 | {MaxWidth, MaxHeight}. 142 | 143 | displayed_length(Line) -> 144 | WithoutEscSeq = remove_escape_sequences(Line), 145 | string:length(WithoutEscSeq). 146 | 147 | remove_escape_sequences(Line) -> 148 | re:replace(Line, "\e\\[[^m]+m", "", [global, {return, list}]). 149 | -------------------------------------------------------------------------------- /test/main_tests.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(main_tests). 9 | 10 | -include_lib("eunit/include/eunit.hrl"). 11 | -include("stdout_formatter.hrl"). 12 | 13 | formatting_test() -> 14 | ?assertEqual( 15 | "string", 16 | stdout_formatter:to_string(["st", ["rin", $g]])), 17 | 18 | ?assertEqual( 19 | "5", 20 | stdout_formatter:to_string(5)), 21 | 22 | ?assertEqual( 23 | "+-+\n" %% +-+ 24 | "|a|\n" %% |a| 25 | "+-+", %% +-+ 26 | stdout_formatter:to_string( 27 | #table{rows = [[a]], 28 | props = #{border_drawing => ascii}})). 29 | 30 | display_test() -> 31 | ?assertEqual(ok, stdout_formatter:display("Hello World!")). 32 | -------------------------------------------------------------------------------- /test/paragraph_tests.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(paragraph_tests). 9 | 10 | -include_lib("eunit/include/eunit.hrl"). 11 | -include("stdout_formatter.hrl"). 12 | 13 | formatting_test() -> 14 | ?assertEqual( 15 | "", 16 | stdout_formatter_paragraph:to_string("")), 17 | 18 | ?assertEqual( 19 | "string", 20 | stdout_formatter_paragraph:to_string("string")), 21 | 22 | ?assertEqual( 23 | "string", 24 | stdout_formatter_paragraph:to_string(["st", ["rin", $g]])), 25 | 26 | ?assertEqual( 27 | "A BBBB C", 28 | stdout_formatter_paragraph:to_string("A\tBBBB\tC")), 29 | 30 | ?assertEqual( 31 | "atom", 32 | stdout_formatter_paragraph:to_string(atom)), 33 | 34 | ?assertEqual( 35 | "binary", 36 | stdout_formatter_paragraph:to_string(<<"binary">>)), 37 | 38 | ?assertEqual( 39 | "1", 40 | stdout_formatter_paragraph:to_string(1)), 41 | 42 | ?assertEqual( 43 | "1.200000", 44 | stdout_formatter_paragraph:to_string(1.2)), 45 | 46 | ?assertEqual( 47 | "{my_tuple,a,b}", 48 | stdout_formatter_paragraph:to_string({my_tuple, a, b})), 49 | 50 | ?assertEqual( 51 | "1.20", 52 | stdout_formatter_paragraph:to_string(#paragraph{content = 1.2, 53 | props = #{format => "~.2.0f"}})), 54 | 55 | ?assertEqual( 56 | "", 57 | stdout_formatter_paragraph:to_string(#table{})), 58 | 59 | ?assertEqual( 60 | "\e(0lqqqqqk\e(B\n" 61 | "\e(0x\e(Btable\e(0x\e(B\n" 62 | "\e(0mqqqqqj\e(B", 63 | stdout_formatter_paragraph:to_string(#table{rows = [[table]]})), 64 | 65 | ?assertEqual( 66 | "Hello World", 67 | stdout_formatter_paragraph:to_string( 68 | #formatted_block{ 69 | lines = [#formatted_line{ 70 | content = ["Hello World"], 71 | props = #{reformat_ok => [{["Hello World"], 11}], 72 | width => 11}}], 73 | props = #{height => 1, width => 11}})). 74 | 75 | concatenation_test() -> 76 | ?assertEqual( 77 | "helloworld", 78 | stdout_formatter_paragraph:to_string(["hello", "world"])), 79 | 80 | ?assertEqual( 81 | "helloworld", 82 | stdout_formatter_paragraph:to_string([#paragraph{content = "hello"}, 83 | "world"])). 84 | 85 | colors_test() -> 86 | ?assertEqual( 87 | "\033[34mBlue fg\033[0m", 88 | stdout_formatter_paragraph:to_string(#paragraph{content = "Blue fg", 89 | props = #{fg => blue}})), 90 | 91 | ?assertEqual( 92 | "\e[38;5;202mOrange fg\e[0m", 93 | stdout_formatter_paragraph:to_string(#paragraph{content = "Orange fg", 94 | props = #{fg => 202}})), 95 | 96 | ?assertEqual( 97 | "\033[43mYellow bg\033[0m", 98 | stdout_formatter_paragraph:to_string(#paragraph{content = "Yellow bg", 99 | props = #{bg => yellow}})), 100 | 101 | ?assertEqual( 102 | "\e[48;5;202mOrange bg\e[0m", 103 | stdout_formatter_paragraph:to_string(#paragraph{content = "Orange bg", 104 | props = #{bg => 202}})), 105 | 106 | ?assertEqual( 107 | "\033[31;42mRed on green\033[0m", 108 | stdout_formatter_paragraph:to_string( 109 | #paragraph{content = "Red on green", 110 | props = #{fg => red, 111 | bg => green}})), 112 | 113 | ?assertEqual( 114 | "\033[38;5;253m\033[48;5;54mGrey on purple\033[0m", 115 | stdout_formatter_paragraph:to_string( 116 | #paragraph{content = "Grey on purple", 117 | props = #{fg => 253, 118 | bg => 54}})), 119 | 120 | ?assertEqual( 121 | "\033[38;2;3;57;108m\033[48;2;100;151;177mSome blues\033[0m", 122 | stdout_formatter_paragraph:to_string(#paragraph{content = "Some blues", 123 | props = #{fg => {3, 57, 108}, 124 | bg => {100, 151, 177} 125 | }})), 126 | 127 | ?assertEqual( 128 | "\033[1mBold\033[0m", 129 | stdout_formatter_paragraph:to_string(#paragraph{content = "Bold", 130 | props = #{bold => true}})), 131 | 132 | ?assertEqual( 133 | "\033[1m\033[31;42mBold + colors\033[0m", 134 | stdout_formatter_paragraph:to_string( 135 | #paragraph{content = "Bold + colors", 136 | props = #{bold => true, 137 | fg => red, 138 | bg => green}})), 139 | ?assertEqual( 140 | #formatted_block{ 141 | lines = [#formatted_line{ 142 | content = ["\e[1;33mYellow!\e[0m"], 143 | props = #{reformat_ok => [{["\e[1;33mYellow!\e[0m"],7}], 144 | width => 7}}], 145 | props = #{height => 1,width => 7}}, 146 | stdout_formatter_paragraph:format("\e[1;33mYellow!\e[0m")). 147 | 148 | wrap_test() -> 149 | ?assertEqual( 150 | "H\ne\nl\nl\no\nw\no\nr\nl\nd", 151 | stdout_formatter_paragraph:to_string( 152 | #paragraph{content = "Hello world", 153 | props = #{wrap_at => 1}})), 154 | 155 | ?assertEqual( 156 | "He\nll\no\nwo\nrl\nd", 157 | stdout_formatter_paragraph:to_string( 158 | #paragraph{content = "Hello world", 159 | props = #{wrap_at => 2}})), 160 | 161 | ?assertEqual( 162 | "Hel\nlo\nwor\nld", 163 | stdout_formatter_paragraph:to_string( 164 | #paragraph{content = "Hello world", 165 | props = #{wrap_at => 3}})), 166 | 167 | ?assertEqual( 168 | "Hell\no\nworl\nd", 169 | stdout_formatter_paragraph:to_string( 170 | #paragraph{content = "Hello world", 171 | props = #{wrap_at => 4}})), 172 | 173 | ?assertEqual( 174 | "Hello\nworld", 175 | stdout_formatter_paragraph:to_string( 176 | #paragraph{content = "Hello world", 177 | props = #{wrap_at => 5}})), 178 | 179 | ?assertEqual( 180 | "Hello\nworld", 181 | stdout_formatter_paragraph:to_string( 182 | #paragraph{content = "Hello world", 183 | props = #{wrap_at => 6}})), 184 | 185 | ?assertEqual( 186 | "Hello\nworld", 187 | stdout_formatter_paragraph:to_string( 188 | #paragraph{content = "Hello world", 189 | props = #{wrap_at => 10}})), 190 | 191 | ?assertEqual( 192 | "Hello world", 193 | stdout_formatter_paragraph:to_string( 194 | #paragraph{content = "Hello world", 195 | props = #{wrap_at => 11}})), 196 | 197 | ?assertEqual( 198 | "Hello world", 199 | stdout_formatter_paragraph:to_string( 200 | #paragraph{content = "Hello world", 201 | props = #{wrap_at => 12}})). 202 | 203 | colors_and_wrap_test() -> 204 | ?assertEqual( 205 | "\033[34mBlue\033[0m\n" 206 | "\033[37mWhite\033[0m\n" 207 | "\033[31mRed\033[0m\n" 208 | "\033[34m-----\033[0m", 209 | stdout_formatter_paragraph:to_string( 210 | #paragraph{content = ["Blue ", 211 | #paragraph{content = "White ", 212 | props = #{fg => white}}, 213 | #paragraph{content = "Red ", 214 | props = #{fg => red}}, 215 | "-----"], 216 | props = #{fg => blue, 217 | wrap_at => 5}})). 218 | 219 | format_test() -> 220 | ?assertEqual( 221 | #formatted_block{ 222 | lines = [#formatted_line{ 223 | content = ["Hello World"], 224 | props = #{reformat_ok => [{["Hello World"], 11}], 225 | width => 11}}], 226 | props = #{height => 1, width => 11}}, 227 | stdout_formatter_paragraph:format("Hello World")). 228 | 229 | display_test() -> 230 | ?assertEqual(ok, stdout_formatter_paragraph:display("Hello World!")). 231 | -------------------------------------------------------------------------------- /test/table_tests.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2019-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(table_tests). 9 | 10 | -include_lib("eunit/include/eunit.hrl"). 11 | -include("stdout_formatter.hrl"). 12 | 13 | format_cell_test() -> 14 | Content = atom, 15 | FormattedBlock = stdout_formatter:format(Content), 16 | ?assertEqual( 17 | #cell{content = FormattedBlock}, 18 | stdout_formatter_table:format_cell(Content)), 19 | ?assertEqual( 20 | #cell{content = FormattedBlock}, 21 | stdout_formatter_table:format_cell(#cell{content = atom})), 22 | ?assertEqual( 23 | #cell{content = FormattedBlock}, 24 | stdout_formatter_table:format_cell(#cell{content = FormattedBlock})). 25 | 26 | normalize_rows_and_cells_test() -> 27 | ?assertEqual( 28 | [], 29 | stdout_formatter_table:normalize_rows_and_cells(#table{}, [])), 30 | 31 | ?assertMatch( 32 | [#row{cells = [], props = _}], 33 | stdout_formatter_table:normalize_rows_and_cells(#table{}, [[]])), 34 | 35 | ?assertMatch( 36 | [#row{ 37 | cells = 38 | [#cell{ 39 | content = 40 | #formatted_block{ 41 | lines = [#formatted_line{content = ["atom"], props = _}], 42 | props = _}, 43 | props = _} 44 | ], 45 | props = _}], 46 | stdout_formatter_table:normalize_rows_and_cells(#table{}, [[atom]])), 47 | 48 | ?assertMatch( 49 | [#row{cells = 50 | [#cell{ 51 | content = 52 | #formatted_block{ 53 | lines = [#formatted_line{content = ["a"], props = _}], 54 | props = _}, 55 | props = _}, 56 | #cell{ 57 | content = 58 | #formatted_block{ 59 | lines = [#formatted_line{content = ["b"], props = _}], 60 | props = _}, 61 | props = _}], 62 | props = _}, 63 | #row{cells = 64 | [#cell{ 65 | content = 66 | #formatted_block{ 67 | lines = [#formatted_line{content = ["c"], props = _}], 68 | props = _}, 69 | props = _}, 70 | #cell{ 71 | content = 72 | #formatted_block{ 73 | lines = [#formatted_line{content = ["d"], props = _}], 74 | props = _}, 75 | props = _}], 76 | props = _}], 77 | stdout_formatter_table:normalize_rows_and_cells( 78 | #table{}, [[a, b], [c, d]])), 79 | 80 | ?assertMatch( 81 | [#row{cells = 82 | [#cell{ 83 | content = 84 | #formatted_block{ 85 | lines = [#formatted_line{content = ["a"], props = _}], 86 | props = _}, 87 | props = _}, 88 | #cell{ 89 | content = 90 | #formatted_block{ 91 | lines = [], 92 | props = _}, 93 | props = _}], 94 | props = _}, 95 | #row{cells = 96 | [#cell{ 97 | content = 98 | #formatted_block{ 99 | lines = [#formatted_line{content = ["c"], props = _}], 100 | props = _}, 101 | props = _}, 102 | #cell{ 103 | content = 104 | #formatted_block{ 105 | lines = [#formatted_line{content = ["d"], props = _}], 106 | props = _}, 107 | props = _}], 108 | props = _}], 109 | stdout_formatter_table:normalize_rows_and_cells( 110 | #table{}, [[a], [c, d]])), 111 | 112 | ?assertMatch( 113 | [#row{cells = 114 | [#cell{ 115 | content = 116 | #formatted_block{ 117 | lines = [#formatted_line{content = ["a"], props = _}], 118 | props = _}, 119 | props = _}, 120 | #cell{ 121 | content = 122 | #formatted_block{ 123 | lines = [#formatted_line{content = ["b"], props = _}], 124 | props = _}, 125 | props = _}], 126 | props = _}, 127 | #row{cells = 128 | [#cell{ 129 | content = 130 | #formatted_block{ 131 | lines = [#formatted_line{content = ["c"], props = _}], 132 | props = _}, 133 | props = _}, 134 | #cell{ 135 | content = 136 | #formatted_block{ 137 | lines = [], 138 | props = _}, 139 | props = _}], 140 | props = _}], 141 | stdout_formatter_table:normalize_rows_and_cells( 142 | #table{}, [[a, b], [c]])), 143 | 144 | ?assertMatch( 145 | [#row{cells = 146 | [#cell{ 147 | content = 148 | #formatted_block{ 149 | lines = [#formatted_line{content = ["a"], props = _}], 150 | props = _}, 151 | props = _}, 152 | #cell{ 153 | content = 154 | #formatted_block{ 155 | lines = [#formatted_line{content = ["b"], props = _}], 156 | props = _}, 157 | props = _}], 158 | props = _}, 159 | #row{cells = 160 | [#cell{ 161 | content = 162 | #formatted_block{ 163 | lines = [#formatted_line{content = ["c"], props = _}], 164 | props = _}, 165 | props = _}, 166 | #cell{ 167 | content = 168 | #formatted_block{ 169 | lines = [], 170 | props = _}, 171 | props = _}], 172 | props = _}, 173 | #row{cells = 174 | [#cell{ 175 | content = 176 | #formatted_block{ 177 | lines = [#formatted_line{content = ["e"], props = _}], 178 | props = _}, 179 | props = _}, 180 | #cell{ 181 | content = 182 | #formatted_block{ 183 | lines = [#formatted_line{content = ["f"], props = _}], 184 | props = _}, 185 | props = _}], 186 | props = _}], 187 | stdout_formatter_table:normalize_rows_and_cells( 188 | #table{}, [[a, b], [c], [e, f]])), 189 | 190 | ?assertMatch( 191 | [#row{cells = 192 | [#cell{ 193 | content = 194 | #formatted_block{ 195 | lines = [#formatted_line{content = ["a"], props = _}], 196 | props = _}, 197 | props = _}, 198 | #cell{ 199 | content = 200 | #formatted_block{ 201 | lines = [#formatted_line{content = ["b"], props = _}], 202 | props = _}, 203 | props = _}, 204 | #cell{ 205 | content = 206 | #formatted_block{ 207 | lines = [], 208 | props = _}, 209 | props = _}, 210 | #cell{ 211 | content = 212 | #formatted_block{ 213 | lines = [], 214 | props = _}, 215 | props = _}], 216 | props = _}, 217 | #row{cells = 218 | [#cell{ 219 | content = 220 | #formatted_block{ 221 | lines = [#formatted_line{content = ["c"], props = _}], 222 | props = _}, 223 | props = _}, 224 | #cell{ 225 | content = 226 | #formatted_block{ 227 | lines = [], 228 | props = _}, 229 | props = _}, 230 | #cell{ 231 | content = 232 | #formatted_block{ 233 | lines = [], 234 | props = _}, 235 | props = _}, 236 | #cell{ 237 | content = 238 | #formatted_block{ 239 | lines = [], 240 | props = _}, 241 | props = _}], 242 | props = _}, 243 | #row{cells = 244 | [#cell{ 245 | content = 246 | #formatted_block{ 247 | lines = [#formatted_line{content = ["e"], props = _}], 248 | props = _}, 249 | props = _}, 250 | #cell{ 251 | content = 252 | #formatted_block{ 253 | lines = [#formatted_line{content = ["f"], props = _}], 254 | props = _}, 255 | props = _}, 256 | #cell{ 257 | content = 258 | #formatted_block{ 259 | lines = [#formatted_line{content = ["g"], props = _}], 260 | props = _}, 261 | props = _}, 262 | #cell{ 263 | content = 264 | #formatted_block{ 265 | lines = [#formatted_line{content = ["h"], props = _}], 266 | props = _}, 267 | props = _}], 268 | props = _}], 269 | stdout_formatter_table:normalize_rows_and_cells( 270 | #table{}, [[a, b], [c], [e, f, g, h]])), 271 | 272 | ?assertMatch( 273 | [#row{cells = 274 | [#cell{ 275 | content = 276 | #formatted_block{ 277 | lines = [#formatted_line{content = ["A"], props = _}, 278 | #formatted_line{content = ["A"], props = _}], 279 | props = _}, 280 | props = _}, 281 | #cell{ 282 | content = 283 | #formatted_block{ 284 | lines = [#formatted_line{content = ["B"], props = _}], 285 | props = _}, 286 | props = _}], 287 | props = _}], 288 | stdout_formatter_table:normalize_rows_and_cells( 289 | #table{}, [["A\nA", "B"]])). 290 | 291 | compute_cols_widths_test() -> 292 | ?assertEqual( 293 | [], 294 | stdout_formatter_table:compute_cols_widths([])), 295 | ?assertEqual( 296 | [], 297 | stdout_formatter_table:compute_cols_widths([#row{}])), 298 | ?assertEqual( 299 | [], 300 | stdout_formatter_table:compute_cols_widths([#row{}, #row{}])), 301 | ?assertEqual( 302 | [0], 303 | stdout_formatter_table:compute_cols_widths( 304 | [#row{cells = [set_default_props(#cell{})]}])), 305 | ?assertEqual( 306 | [0], 307 | stdout_formatter_table:compute_cols_widths( 308 | [#row{cells = [set_default_props(#cell{})]}, 309 | #row{cells = [set_default_props(#cell{})]}])), 310 | ?assertEqual( 311 | [3], 312 | stdout_formatter_table:compute_cols_widths( 313 | [#row{cells = [format_cell(a)]}, 314 | #row{cells = [format_cell(bcd)]}])), 315 | ?assertEqual( 316 | [3], 317 | stdout_formatter_table:compute_cols_widths( 318 | [#row{cells = [format_cell(abc)]}, 319 | #row{cells = [format_cell(d)]}])), 320 | ?assertEqual( 321 | [0, 3], 322 | stdout_formatter_table:compute_cols_widths( 323 | [#row{cells = [set_default_props(#cell{}), format_cell(abc)]}, 324 | #row{cells = [set_default_props(#cell{}), format_cell(d)]}])), 325 | ?assertEqual( 326 | [3, 0], 327 | stdout_formatter_table:compute_cols_widths( 328 | [#row{cells = [format_cell(abc), set_default_props(#cell{})]}, 329 | #row{cells = [format_cell(d), set_default_props(#cell{})]}])), 330 | ?assertEqual( 331 | [1], 332 | stdout_formatter_table:compute_cols_widths( 333 | [#row{cells = [format_cell("a\nb")]}, 334 | #row{cells = [format_cell(c)]}])), 335 | ?assertEqual( 336 | [3], 337 | stdout_formatter_table:compute_cols_widths( 338 | [#row{cells = [format_cell("a\nbcd")]}, 339 | #row{cells = [format_cell(e)]}])). 340 | 341 | set_default_props(#table{} = Table) -> 342 | stdout_formatter_table:set_default_table_props(Table, #{}); 343 | set_default_props(#row{} = Row) -> 344 | stdout_formatter_table:set_default_row_props(Row, #{}); 345 | set_default_props(#cell{} = Cell) -> 346 | stdout_formatter_table:set_default_cell_props(Cell, #{}). 347 | 348 | format_cell(Term) -> 349 | set_default_props(stdout_formatter_table:format_cell(Term)). 350 | 351 | to_string_test() -> 352 | ?assertEqual( 353 | "", 354 | stdout_formatter_table:to_string([])), 355 | ?assertEqual( 356 | "\e(0lqk\e(B\n" %% ┌─┐ 357 | "\e(0x\e(Ba\e(0x\e(B\n" %% │a│ 358 | "\e(0mqj\e(B", %% └─┘ 359 | stdout_formatter_table:to_string([[a]])), 360 | ?assertEqual( 361 | "\e(0lqk\e(B\n" %% ┌─┐ 362 | "\e(0x\e(Ba\e(0x\e(B\n" %% │a│ 363 | "\e(0mqj\e(B", %% └─┘ 364 | stdout_formatter_table:to_string( 365 | #table{rows = [[a]], 366 | props = #{border_drawing => ansi}})), 367 | ?assertEqual( 368 | "+-+\n" %% +-+ 369 | "|a|\n" %% |a| 370 | "+-+", %% +-+ 371 | stdout_formatter_table:to_string( 372 | #table{rows = [[a]], 373 | props = #{border_drawing => ascii}})), 374 | ?assertEqual( 375 | "\e(0lqwqk\e(B\n" %% ┌─┬─┐ 376 | "\e(0x\e(Ba\e(0x\e(Bb\e(0x\e(B\n" %% │a│b│ 377 | "\e(0tqnqu\e(B\n" %% ├─┼─┤ 378 | "\e(0x\e(Bc\e(0x\e(Bd\e(0x\e(B\n" %% │c│d│ 379 | "\e(0mqvqj\e(B", %% └─┴─┘ 380 | stdout_formatter_table:to_string([[a, b], [c, d]])), 381 | ?assertEqual( 382 | "+-+-+\n" %% +-+-+ 383 | "|a|b|\n" %% |a|b| 384 | "+-+-+\n" %% +-+-+ 385 | "|c|d|\n" %% |c|d| 386 | "+-+-+", %% +-+-+ 387 | stdout_formatter_table:to_string( 388 | #table{rows = [[a, b], [c, d]], 389 | props = #{border_drawing => ascii}})), 390 | ?assertEqual( 391 | "+-+-+\n" %% +-+-+ 392 | "|\033[1ma\033[0m|\033[1mb\033[0m|\n" %% |a|b| 393 | "+-+-+\n" %% +-+-+ 394 | "|c|d|\n" %% |c|d| 395 | "+-+-+", %% +-+-+ 396 | stdout_formatter_table:to_string( 397 | #table{rows = [#row{cells = [a, b], props = #{title => true}}, 398 | [c, d]], 399 | props = #{border_drawing => ascii}})), 400 | ?assertEqual( 401 | "+-+-+\n" %% +-+-+ 402 | "|A|B|\n" %% |A|B| 403 | "|A| |\n" %% |A| | 404 | "+-+-+", %% +-+-+ 405 | stdout_formatter_table:to_string( 406 | #table{rows = [["A\nA", "B"]], 407 | props = #{border_drawing => ascii}})), 408 | ?assertEqual( 409 | "+-+-+\n" %% +-+-+ 410 | "|A|B|\n" %% |A|B| 411 | "| |B|\n" %% | |B| 412 | "+-+-+", %% +-+-+ 413 | stdout_formatter_table:to_string( 414 | #table{rows = [["A", "B\nB"]], 415 | props = #{border_drawing => ascii}})), 416 | ?assertEqual( 417 | "AB\n" 418 | " B", 419 | stdout_formatter_table:to_string( 420 | #table{rows = [["A", "B\nB"]], 421 | props = #{border_drawing => none}})), 422 | ?assertEqual( 423 | "+---+---+\n" 424 | "| | |\n" 425 | "| A | B |\n" 426 | "| | B |\n" 427 | "| | |\n" 428 | "+---+---+", 429 | stdout_formatter_table:to_string( 430 | #table{rows = [["A", "B\nB"]], 431 | props = #{border_drawing => ascii, 432 | cell_padding => 1}})), 433 | ?assertEqual( 434 | "+-+-+\n" 435 | "| | |\n" 436 | "| | |\n" 437 | "|A|B|\n" 438 | "| |B|\n" 439 | "| | |\n" 440 | "| | |\n" 441 | "+-+-+", 442 | stdout_formatter_table:to_string( 443 | #table{rows = [["A", "B\nB"]], 444 | props = #{border_drawing => ascii, 445 | cell_padding => {2, 0}}})), 446 | ?assertEqual( 447 | "+-----+-----+\n" 448 | "| A | B |\n" 449 | "| | B |\n" 450 | "+-----+-----+", 451 | stdout_formatter_table:to_string( 452 | #table{rows = [["A", "B\nB"]], 453 | props = #{border_drawing => ascii, 454 | cell_padding => {0, 2}}})), 455 | ?assertEqual( 456 | "+-------+-------+\n" 457 | "| | |\n" 458 | "| A | B |\n" 459 | "| | B |\n" 460 | "| | |\n" 461 | "| | |\n" 462 | "| | |\n" 463 | "+-------+-------+", 464 | stdout_formatter_table:to_string( 465 | #table{rows = [["A", "B\nB"]], 466 | props = #{border_drawing => ascii, 467 | cell_padding => {1, 2, 3, 4}}})), 468 | ?assertEqual( 469 | "+-------+-+\n" 470 | "| | |\n" 471 | "| A |B|\n" 472 | "| |B|\n" 473 | "| | |\n" 474 | "| | |\n" 475 | "| | |\n" 476 | "+-------+-+", 477 | stdout_formatter_table:to_string( 478 | #table{rows = [[#cell{content = "A", 479 | props = #{padding => {1, 2, 3, 4}}}, 480 | "B\nB"]], 481 | props = #{border_drawing => ascii}})), 482 | ?assertEqual( 483 | "+-+-------+\n" 484 | "|a| b |\n" 485 | "+-+-------+\n" 486 | "| | |\n" 487 | "|c| d |\n" 488 | "| | |\n" 489 | "| | |\n" 490 | "| | |\n" 491 | "+-+-------+", 492 | stdout_formatter_table:to_string( 493 | #table{rows = [[a, b], 494 | [c, 495 | #cell{content = "d", 496 | props = #{padding => {1, 2, 3, 4}}}]], 497 | props = #{border_drawing => ascii}})). 498 | 499 | format_test() -> 500 | ?assertEqual( 501 | #formatted_block{ 502 | lines = [#formatted_line{ 503 | content = "\e(0lqk\e(B", 504 | props = #{reformat_ok => false, width => 3}}, 505 | #formatted_line{ 506 | content = ["\e(0x\e(B",[[],["a"],[]],"\e(0x\e(B"], 507 | props = #{reformat_ok => false, width => 3}}, 508 | #formatted_line{ 509 | content = "\e(0mqj\e(B", 510 | props = #{reformat_ok => false, width => 3}} 511 | ], 512 | props = #{height => 3, width => 3}}, 513 | stdout_formatter_table:format([[a]])). 514 | 515 | display_test() -> 516 | ?assertEqual(ok, stdout_formatter_table:display([[a, b]])). 517 | --------------------------------------------------------------------------------