├── .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 | [](https://travis-ci.com/rabbitmq/stdout_formatter)
4 | [](https://coveralls.io/github/rabbitmq/stdout_formatter)
5 | [](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 | %% - {@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.
17 | %% - {@link to_string/1} formats the given argument and returns a
18 | %% string ready to be displayed or stored.
19 | %% - {@link display/1} does the same thing as {@link to_string/1} but
20 | %% displays the result on `stdout' directly (and returns nothing).
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 | %%
88 | %% - `format': the format string to present the content.
89 | %% - `wrap_at': the number of columns after which the lines are wrapped.
90 | %% - `bold': `true' if the content must be dispalyed as bold characters.
91 | %% - `fg': the color to use as foreground.
92 | %% - `bg': the color to use as background.
93 | %%
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 | %%
110 | %% - `border_drawing': the type of line drawing to use.
111 | %% - `border_style': the style of borders.
112 | %%
113 |
114 | -type border_drawing() :: ansi | ascii | none.
115 | %% The line drawing technique.
116 | %%
117 | %%
118 | %% - `ansi': borders are drawn with ANSI escape sequences.
119 | %% - `ascii': borders are drawn with US-ASCII characters.
120 | %% - `none': no border drawn.
121 | %%
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 | %%
145 | %% - `title': `true' if cells are all title cells.
146 | %% - `title_repeat': whether to repeat the title rows and how often.
147 | %% [NOT IMPLEMENTED]
148 | %%
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 | %%
162 | %% - `title': `true' if the cells is a title (i.e. content is bold).
163 | %%
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 | %%
177 | %% - `width': Number of columns of the widest line.
178 | %% - `height': Number of lines.
179 | %%
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 | %%
190 | %% - `width': Number of columns of the line.
191 | %% - `reformat_ok': Content used to reformat the line if relevant,
192 | %% e.g. to rewrap a paragraph.
193 | %%
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 | %% - the initial properties map
31 | %% - the inherited properties
32 | %% - the default properties
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 |
--------------------------------------------------------------------------------