From 1584112d17806ccd9f6a5f88fcd1987bdaa39fee Mon Sep 17 00:00:00 2001 From: Packit Date: Sep 16 2020 16:13:33 +0000 Subject: perl-Term-Table-0.012 base --- diff --git a/Changes b/Changes new file mode 100644 index 0000000..cc408fa --- /dev/null +++ b/Changes @@ -0,0 +1,48 @@ +0.012 2017-10-11 08:54:52-07:00 America/Los_Angeles + + - Bump minimum Test2::Tools::Tiny version + +0.011 2017-10-09 12:52:32-07:00 America/Los_Angeles + + - Honor the env variable even on non-term output + +0.010 2017-10-03 09:41:46-07:00 America/Los_Angeles + + - No changes since trial + +0.009 2017-09-18 20:51:26-07:00 America/Los_Angeles (TRIAL RELEASE) + + - Further optimize term_size when SIGWINCH is supported + +0.008 2017-03-17 10:08:18-07:00 America/Los_Angeles + + - Prefer Term::Size::Any over Term::ReadKey + - Do not use either if STDOUT is not a terminal + +0.007 2017-03-08 20:15:13-08:00 America/Los_Angeles (TRIAL RELEASE) + + - Remove stray line that was setting STDOUT to utf8 (wtf?) + +0.006 2017-01-10 21:13:35-08:00 America/Los_Angeles + + - Fix rendering issue when all lines are the same length + +0.005 2016-12-27 23:04:11-08:00 America/Los_Angeles + + - Add spacer + +0.004 2016-12-20 18:13:50-08:00 America/Los_Angeles + + - Add cellstack + +0.003 2016-12-20 06:23:43-08:00 America/Los_Angeles + + - Fix recommended deps + +0.002 2016-12-19 12:23:51-08:00 America/Los_Angeles + + - Fix a broken test. + +0.001 2016-12-18 23:01:20-08:00 America/Los_Angeles + + - Initial Release (moved out of Test2-Suite) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c66584 --- /dev/null +++ b/LICENSE @@ -0,0 +1,379 @@ +This software is copyright (c) 2017 by Chad Granum. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +Terms of the Perl programming language system itself + +a) the GNU General Public License as published by the Free + Software Foundation; either version 1, or (at your option) any + later version, or +b) the "Artistic License" + +--- The GNU General Public License, Version 1, February 1989 --- + +This software is Copyright (c) 2017 by Chad Granum. + +This is free software, licensed under: + + The GNU General Public License, Version 1, February 1989 + + GNU GENERAL PUBLIC LICENSE + Version 1, February 1989 + + Copyright (C) 1989 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The license agreements of most software companies try to keep users +at the mercy of those companies. By contrast, our General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. The +General Public License applies to the Free Software Foundation's +software and to any other program whose authors commit to using it. +You can use it for your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Specifically, the General Public License is designed to make +sure that you have the freedom to give away or sell copies of free +software, that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free +programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of a such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any program or other work which +contains a notice placed by the copyright holder saying it may be +distributed under the terms of this General Public License. The +"Program", below, refers to any such program or work, and a "work based +on the Program" means either the Program or any work containing the +Program or a portion of it, either verbatim or with modifications. Each +licensee is addressed as "you". + + 1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this +General Public License and to the absence of any warranty; and give any +other recipients of the Program a copy of this General Public License +along with the Program. You may charge a fee for the physical act of +transferring a copy. + + 2. You may modify your copy or copies of the Program or any portion of +it, and copy and distribute such modifications under the terms of Paragraph +1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating that + you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, that + in whole or in part contains the Program or any part thereof, either + with or without modifications, to be licensed at no charge to all + third parties under the terms of this General Public License (except + that you may choose to grant warranty protection to some or all + third parties, at your option). + + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the simplest and most usual way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this General + Public License. + + d) You may charge a fee for the physical act of transferring a + copy, and you may at your option offer warranty protection in + exchange for a fee. + +Mere aggregation of another independent work with the Program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other work under the scope of these terms. + + 3. You may copy and distribute the Program (or a portion or derivative of +it, under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal charge + for the cost of distribution) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form alone.) + +Source code for a work means the preferred form of the work for making +modifications to it. For an executable file, complete source code means +all the source code for all modules it contains; but, as a special +exception, it need not include source code for modules which are standard +libraries that accompany the operating system on which the executable +file runs, or for standard header files or definitions files that +accompany that operating system. + + 4. You may not copy, modify, sublicense, distribute or transfer the +Program except as expressly provided under this General Public License. +Any attempt otherwise to copy, modify, sublicense, distribute or transfer +the Program is void, and will automatically terminate your rights to use +the Program under this License. However, parties who have received +copies, or rights to use copies, from you under this General Public +License will not have their licenses terminated so long as such parties +remain in full compliance. + + 5. By copying, distributing or modifying the Program (or any work based +on the Program) you indicate your acceptance of this license to do so, +and all its terms and conditions. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these +terms and conditions. You may not impose any further restrictions on the +recipients' exercise of the rights granted herein. + + 7. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of the license which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +the license, you may choose any version ever published by the Free Software +Foundation. + + 8. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to humanity, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19xx name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than `show w' and `show +c'; they could even be mouse-clicks or menu items--whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (a program to direct compilers to make passes + at assemblers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +That's all there is to it! + + +--- The Artistic License 1.0 --- + +This software is Copyright (c) 2017 by Chad Granum. + +This is free software, licensed under: + + The Artistic License 1.0 + +The Artistic License + +Preamble + +The intent of this document is to state the conditions under which a Package +may be copied, such that the Copyright Holder maintains some semblance of +artistic control over the development of the package, while giving the users of +the package the right to use and distribute the Package in a more-or-less +customary fashion, plus the right to make reasonable modifications. + +Definitions: + + - "Package" refers to the collection of files distributed by the Copyright + Holder, and derivatives of that collection of files created through + textual modification. + - "Standard Version" refers to such a Package if it has not been modified, + or has been modified in accordance with the wishes of the Copyright + Holder. + - "Copyright Holder" is whoever is named in the copyright or copyrights for + the package. + - "You" is you, if you're thinking about copying or distributing this Package. + - "Reasonable copying fee" is whatever you can justify on the basis of media + cost, duplication charges, time of people involved, and so on. (You will + not be required to justify it to the Copyright Holder, but only to the + computing community at large as a market that must bear the fee.) + - "Freely Available" means that no fee is charged for the item itself, though + there may be fees involved in handling the item. It also means that + recipients of the item may redistribute it under the same conditions they + received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications derived +from the Public Domain or from the Copyright Holder. A Package modified in such +a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided that +you insert a prominent notice in each changed file stating how and when you +changed that file, and provided that you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or placing the modifications on a major archive site + such as ftp.uu.net, or by allowing the Copyright Holder to include your + modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided, and provide a separate + manual page for each non-standard executable that clearly documents how it + differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or executable +form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where to + get the Standard Version. + + b) accompany the distribution with the machine-readable source of the Package + with your modifications. + + c) accompany any non-standard executables with their corresponding Standard + Version executables, giving the non-standard executables non-standard + names, and clearly documenting the differences in manual pages (or + equivalent), together with instructions on where to get the Standard + Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. You +may not charge a fee for this Package itself. However, you may distribute this +Package in aggregate with other (possibly commercial) programs as part of a +larger (possibly commercial) software distribution provided that you do not +advertise this Package as a product of your own. + +6. The scripts and library files supplied as input to or produced as output +from the programs of this Package do not automatically fall under the copyright +of this Package, but belong to whomever generated them, and may be sold +commercially, and may be aggregated with this Package. + +7. C or perl subroutines supplied by you and linked into this Package shall not +be considered part of this Package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +The End + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..ca9c2e3 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,25 @@ +# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.009. +Changes +LICENSE +MANIFEST +META.json +META.yml +Makefile.PL +README +README.md +appveyor.yml +cpanfile +lib/Term/Table.pm +lib/Term/Table/Cell.pm +lib/Term/Table/CellStack.pm +lib/Term/Table/HashBase.pm +lib/Term/Table/LineBreak.pm +lib/Term/Table/Spacer.pm +lib/Term/Table/Util.pm +t/HashBase.t +t/Table.t +t/Table/Cell.t +t/Table/CellStack.t +t/Table/LineBreak.t +t/bad_blank_line.t +t/honor_env_in_non_tty.t diff --git a/META.json b/META.json new file mode 100644 index 0000000..c3d04d0 --- /dev/null +++ b/META.json @@ -0,0 +1,61 @@ +{ + "abstract" : "Format a header and rows into a table", + "author" : [ + "Chad Granum " + ], + "dynamic_config" : 0, + "generated_by" : "Dist::Zilla version 6.009, CPAN::Meta::Converter version 2.150010", + "license" : [ + "perl_5" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : 2 + }, + "name" : "Term-Table", + "prereqs" : { + "configure" : { + "requires" : { + "ExtUtils::MakeMaker" : "0" + } + }, + "develop" : { + "requires" : { + "Test::Pod" : "1.41" + } + }, + "runtime" : { + "recommends" : { + "Term::Size::Any" : "0.002", + "Unicode::GCString" : "2013.10", + "Unicode::LineBreak" : "2015.06" + }, + "requires" : { + "Carp" : "0", + "Importer" : "0.024", + "List::Util" : "0", + "Scalar::Util" : "0", + "perl" : "5.008001" + } + }, + "test" : { + "requires" : { + "Test2::Tools::Tiny" : "1.302097", + "utf8" : "0" + } + } + }, + "release_status" : "stable", + "resources" : { + "bugtracker" : { + "web" : "http://github.com/exodist/Term-Table/issues" + }, + "repository" : { + "type" : "git", + "url" : "http://github.com/exodist/Term-Table/" + } + }, + "version" : "0.012", + "x_serialization_backend" : "Cpanel::JSON::XS version 3.0233" +} + diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..0234b36 --- /dev/null +++ b/META.yml @@ -0,0 +1,31 @@ +--- +abstract: 'Format a header and rows into a table' +author: + - 'Chad Granum ' +build_requires: + Test2::Tools::Tiny: '1.302097' + utf8: '0' +configure_requires: + ExtUtils::MakeMaker: '0' +dynamic_config: 0 +generated_by: 'Dist::Zilla version 6.009, CPAN::Meta::Converter version 2.150010' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: Term-Table +recommends: + Term::Size::Any: '0.002' + Unicode::GCString: '2013.10' + Unicode::LineBreak: '2015.06' +requires: + Carp: '0' + Importer: '0.024' + List::Util: '0' + Scalar::Util: '0' + perl: '5.008001' +resources: + bugtracker: http://github.com/exodist/Term-Table/issues + repository: http://github.com/exodist/Term-Table/ +version: '0.012' +x_serialization_backend: 'YAML::Tiny version 1.70' diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..2cb83a9 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,55 @@ +# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.009. +use strict; +use warnings; + +use 5.008001; + +use ExtUtils::MakeMaker; + +my %WriteMakefileArgs = ( + "ABSTRACT" => "Format a header and rows into a table", + "AUTHOR" => "Chad Granum ", + "CONFIGURE_REQUIRES" => { + "ExtUtils::MakeMaker" => 0 + }, + "DISTNAME" => "Term-Table", + "LICENSE" => "perl", + "MIN_PERL_VERSION" => "5.008001", + "NAME" => "Term::Table", + "PREREQ_PM" => { + "Carp" => 0, + "Importer" => "0.024", + "List::Util" => 0, + "Scalar::Util" => 0 + }, + "TEST_REQUIRES" => { + "Test2::Tools::Tiny" => "1.302097", + "utf8" => 0 + }, + "VERSION" => "0.012", + "test" => { + "TESTS" => "t/*.t t/Table/*.t" + } +); + + +my %FallbackPrereqs = ( + "Carp" => 0, + "Importer" => "0.024", + "List::Util" => 0, + "Scalar::Util" => 0, + "Test2::Tools::Tiny" => "1.302097", + "utf8" => 0 +); + + +unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { + delete $WriteMakefileArgs{TEST_REQUIRES}; + delete $WriteMakefileArgs{BUILD_REQUIRES}; + $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; +} + +delete $WriteMakefileArgs{CONFIGURE_REQUIRES} + unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; + +WriteMakefile(%WriteMakefileArgs); diff --git a/README b/README new file mode 100644 index 0000000..fc30f63 --- /dev/null +++ b/README @@ -0,0 +1,171 @@ +NAME + + Term::Table - Format a header and rows into a table + +DESCRIPTION + + This is used by some failing tests to provide diagnostics about what + has gone wrong. This module is able to generic format rows of data into + tables. + +SYNOPSIS + + use Term::Table; + + my $table = Term::Table->new( + max_width => 80, # defaults to terminal size + collapse => 1, # do not show empty columns + header => [ 'name', 'age', 'hair color' ], + rows => [ + [ 'Fred Flinstone', 2000000, 'black' ], + [ 'Wilma Flinstone', 1999995, 'red' ], + ... + ], + ); + + say $_ for $table->render; + + This prints a table like this: + + +-----------------+---------+------------+ + | name | age | hair color | + +-----------------+---------+------------+ + | Fred Flinstone | 2000000 | black | + | Wilma Flinstone | 1999995 | red | + | ... | ... | ... | + +-----------------+---------+------------+ + +INTERFACE + + use Term::Table; + my $table = Term::Table->new(...); + + OPTIONS + + header => [ ... ] + + If you want a header specify it here. This takes an arrayref with + each columns heading. + + rows => [ [...], [...], ... ] + + This should be an arrayref containing an arrayref per row. + + collapse => $bool + + Use this if you want to hide empty columns, that is any column that + has no data in any row. Having a header for the column will not + effect collapse. + + max_width => $num + + Set the maximum width of the table, the table may not be this big, + but it will be no bigger. If none is specified it will attempt to + find the width of your terminal and use that, otherwise it falls back + to the terminal width or 80. + + sanitize => $bool + + This will sanitize all the data in the table such that newlines, + control characters, and all whitespace except for ASCII 20 ' ' are + replaced with escape sequences. This prevents newlines, tabs, and + similar whitespace from disrupting the table. + + Note: newlines are marked as '\n', but a newline is also inserted + into the data so that it typically displays in a way that is useful + to humans. + + Example: + + my $field = "foo\nbar\nbaz\n"; + + print join "\n" => table( + sanitize => 1, + rows => [ + [$field, 'col2' ], + ['row2 col1', 'row2 col2'] + ] + ); + + Prints: + + +-----------------+-----------+ + | foo\n | col2 | + | bar\n | | + | baz\n | | + | | | + | row2 col1 | row2 col2 | + +-----------------+-----------+ + + So it marks the newlines by inserting the escape sequence, but it + also shows the data across as many lines as it would normally + display. + + mark_tail => $bool + + This will replace the last whitespace character of any trailing + whitespace with its escape sequence. This makes it easier to notice + trailing whitespace when comparing values. + + show_header => $bool + + Set this to false to hide the header. This defaults to true if the + header is set, false if no header is provided. + + auto_columns => $bool + + Set this to true to automatically add columns that are not named in + the header. This defaults to false if a header is provided, and + defaults to true when there is no header. + + no_collapse => [ $col_num_a, $col_num_b, ... ] + + no_collapse => [ $col_name_a, $col_name_b, ... ] + + no_collapse => { $col_num_a => 1, $col_num_b => 1, ... } + + no_collapse => { $col_name_a => 1, $col_name_b => 1, ... } + + Specify (by number and/or name) columns that should not be removed + when empty. The 'name' form only works when a header is specified. + There is currently no protection to insure that names you specify are + actually in the header, invalid names are ignored, patches to fix + this will be happily accepted. + +NOTE ON UNICODE/WIDE CHARACTERS + + Some unicode characters, such as 婧 (U+5A67) are wider than others. + These will render just fine if you use utf8; as necessary, and + Unicode::GCString is installed, however if the module is not installed + there will be anomalies in the table: + + +-----+-----+---+ + | a | b | c | + +-----+-----+---+ + | 婧 | x | y | + | x | y | z | + | x | 婧 | z | + +-----+-----+---+ + +SOURCE + + The source code repository for Term-Table can be found at + http://github.com/exodist/Term-Table/. + +MAINTAINERS + + Chad Granum + +AUTHORS + + Chad Granum + +COPYRIGHT + + Copyright 2016 Chad Granum . + + This program is free software; you can redistribute it and/or modify it + under the same terms as Perl itself. + + See http://dev.perl.org/licenses/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7043196 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# NAME + +Term::Table - Format a header and rows into a table + +# DESCRIPTION + +This is used by some failing tests to provide diagnostics about what has gone +wrong. This module is able to generic format rows of data into tables. + +# SYNOPSIS + + use Term::Table; + + my $table = Term::Table->new( + max_width => 80, # defaults to terminal size + collapse => 1, # do not show empty columns + header => [ 'name', 'age', 'hair color' ], + rows => [ + [ 'Fred Flinstone', 2000000, 'black' ], + [ 'Wilma Flinstone', 1999995, 'red' ], + ... + ], + ); + + say $_ for $table->render; + +This prints a table like this: + + +-----------------+---------+------------+ + | name | age | hair color | + +-----------------+---------+------------+ + | Fred Flinstone | 2000000 | black | + | Wilma Flinstone | 1999995 | red | + | ... | ... | ... | + +-----------------+---------+------------+ + +# INTERFACE + + use Term::Table; + my $table = Term::Table->new(...); + +## OPTIONS + +- header => \[ ... \] + + If you want a header specify it here. This takes an arrayref with each columns + heading. + +- rows => \[ \[...\], \[...\], ... \] + + This should be an arrayref containing an arrayref per row. + +- collapse => $bool + + Use this if you want to hide empty columns, that is any column that has no data + in any row. Having a header for the column will not effect collapse. + +- max\_width => $num + + Set the maximum width of the table, the table may not be this big, but it will + be no bigger. If none is specified it will attempt to find the width of your + terminal and use that, otherwise it falls back to the terminal width or `80`. + +- sanitize => $bool + + This will sanitize all the data in the table such that newlines, control + characters, and all whitespace except for ASCII 20 `' '` are replaced with + escape sequences. This prevents newlines, tabs, and similar whitespace from + disrupting the table. + + **Note:** newlines are marked as '\\n', but a newline is also inserted into the + data so that it typically displays in a way that is useful to humans. + + Example: + + my $field = "foo\nbar\nbaz\n"; + + print join "\n" => table( + sanitize => 1, + rows => [ + [$field, 'col2' ], + ['row2 col1', 'row2 col2'] + ] + ); + + Prints: + + +-----------------+-----------+ + | foo\n | col2 | + | bar\n | | + | baz\n | | + | | | + | row2 col1 | row2 col2 | + +-----------------+-----------+ + + So it marks the newlines by inserting the escape sequence, but it also shows + the data across as many lines as it would normally display. + +- mark\_tail => $bool + + This will replace the last whitespace character of any trailing whitespace with + its escape sequence. This makes it easier to notice trailing whitespace when + comparing values. + +- show\_header => $bool + + Set this to false to hide the header. This defaults to true if the header is + set, false if no header is provided. + +- auto\_columns => $bool + + Set this to true to automatically add columns that are not named in the header. + This defaults to false if a header is provided, and defaults to true when there + is no header. + +- no\_collapse => \[ $col\_num\_a, $col\_num\_b, ... \] +- no\_collapse => \[ $col\_name\_a, $col\_name\_b, ... \] +- no\_collapse => { $col\_num\_a => 1, $col\_num\_b => 1, ... } +- no\_collapse => { $col\_name\_a => 1, $col\_name\_b => 1, ... } + + Specify (by number and/or name) columns that should not be removed when empty. + The 'name' form only works when a header is specified. There is currently no + protection to insure that names you specify are actually in the header, invalid + names are ignored, patches to fix this will be happily accepted. + +# NOTE ON UNICODE/WIDE CHARACTERS + +Some unicode characters, such as `婧` (`U+5A67`) are wider than others. These +will render just fine if you `use utf8;` as necessary, and +[Unicode::GCString](https://metacpan.org/pod/Unicode::GCString) is installed, however if the module is not installed there +will be anomalies in the table: + + +-----+-----+---+ + | a | b | c | + +-----+-----+---+ + | 婧 | x | y | + | x | y | z | + | x | 婧 | z | + +-----+-----+---+ + +# SOURCE + +The source code repository for Term-Table can be found at +`http://github.com/exodist/Term-Table/`. + +# MAINTAINERS + +- Chad Granum + +# AUTHORS + +- Chad Granum + +# COPYRIGHT + +Copyright 2016 Chad Granum . + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See `http://dev.perl.org/licenses/` diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..e500923 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,27 @@ +skip_tags: true + +cache: + - C:\strawberry + +install: + - if not exist "C:\strawberry" cinst strawberryperl -y + - set PATH=C:\strawberry\perl\bin;C:\strawberry\perl\site\bin;C:\strawberry\c\bin;%PATH% + - cd C:\projects\%APPVEYOR_PROJECT_NAME% + - cpanm -n Dist::Zilla Pod::Markdown + - dzil authordeps --missing | cpanm -n + - dzil listdeps --author --missing | cpanm + +build_script: + - perl -e 2 + +test_script: + - dzil test + +notifications: + - provider: Slack + auth_token: + secure: 1XmVVszAQyTtMdNkyWup8p7AC9iqXkMl6QMchq3Xu7L7rCzYgjjlS/mas+bfp3ouyjPKnoh01twl4eB0Xs/1Ig== + channel: '#general' + on_build_success: false + on_build_failure: true + on_build_status_changed: true diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..2d16be3 --- /dev/null +++ b/cpanfile @@ -0,0 +1,21 @@ +requires "Carp" => "0"; +requires "Importer" => "0.024"; +requires "List::Util" => "0"; +requires "Scalar::Util" => "0"; +requires "perl" => "5.008001"; +recommends "Term::Size::Any" => "0.002"; +recommends "Unicode::GCString" => "2013.10"; +recommends "Unicode::LineBreak" => "2015.06"; + +on 'test' => sub { + requires "Test2::Tools::Tiny" => "1.302097"; + requires "utf8" => "0"; +}; + +on 'configure' => sub { + requires "ExtUtils::MakeMaker" => "0"; +}; + +on 'develop' => sub { + requires "Test::Pod" => "1.41"; +}; diff --git a/lib/Term/Table.pm b/lib/Term/Table.pm new file mode 100644 index 0000000..bdade69 --- /dev/null +++ b/lib/Term/Table.pm @@ -0,0 +1,425 @@ +package Term::Table; +use strict; +use warnings; + +our $VERSION = '0.012'; + +use Term::Table::Cell(); + +use Term::Table::Util qw/term_size uni_length USE_GCS/; +use Scalar::Util qw/blessed/; +use List::Util qw/max sum/; +use Carp qw/croak carp/; + +use Term::Table::HashBase qw/rows _columns collapse max_width mark_tail sanitize show_header auto_columns no_collapse header/; + +sub BORDER_SIZE() { 4 } # '| ' and ' |' borders +sub DIV_SIZE() { 3 } # ' | ' column delimiter +sub PAD_SIZE() { 4 } # Extra arbitrary padding +sub CELL_PAD_SIZE() { 2 } # space on either side of the | + +sub init { + my $self = shift; + + croak "You cannot have a table with no rows" + unless $self->{+ROWS} && @{$self->{+ROWS}}; + + $self->{+MAX_WIDTH} ||= term_size(); + $self->{+NO_COLLAPSE} ||= {}; + if (ref($self->{+NO_COLLAPSE}) eq 'ARRAY') { + $self->{+NO_COLLAPSE} = {map { ($_ => 1) } @{$self->{+NO_COLLAPSE}}}; + } + + if ($self->{+NO_COLLAPSE} && $self->{+HEADER}) { + my $header = $self->{+HEADER}; + for(my $idx = 0; $idx < @$header; $idx++) { + $self->{+NO_COLLAPSE}->{$idx} ||= $self->{+NO_COLLAPSE}->{$header->[$idx]}; + } + } + + $self->{+COLLAPSE} = 1 unless defined $self->{+COLLAPSE}; + $self->{+SANITIZE} = 1 unless defined $self->{+SANITIZE}; + $self->{+MARK_TAIL} = 1 unless defined $self->{+MARK_TAIL}; + + if($self->{+HEADER}) { + $self->{+SHOW_HEADER} = 1 unless defined $self->{+SHOW_HEADER}; + } + else { + $self->{+HEADER} = []; + $self->{+AUTO_COLUMNS} = 1; + $self->{+SHOW_HEADER} = 0; + } +} + +sub columns { + my $self = shift; + + $self->regen_columns unless $self->{+_COLUMNS}; + + return $self->{+_COLUMNS}; +} + +sub regen_columns { + my $self = shift; + + my $has_header = $self->{+SHOW_HEADER} && @{$self->{+HEADER}}; + my %new_col = (width => 0, count => $has_header ? -1 : 0); + + my $cols = [map { {%new_col} } @{$self->{+HEADER}}]; + my @rows = @{$self->{+ROWS}}; + + for my $row ($has_header ? ($self->{+HEADER}, @rows) : (@rows)) { + for my $ci (0 .. max(@$cols - 1, @$row - 1)) { + $cols->[$ci] ||= {%new_col} if $self->{+AUTO_COLUMNS}; + my $c = $cols->[$ci] or next; + $c->{idx} ||= $ci; + $c->{rows} ||= []; + + my $r = $row->[$ci]; + $r = Term::Table::Cell->new(value => $r) + unless blessed($r) + && ($r->isa('Term::Table::Cell') + || $r->isa('Term::Table::CellStack') + || $r->isa('Term::Table::Spacer')); + + $r->sanitize if $self->{+SANITIZE}; + $r->mark_tail if $self->{+MARK_TAIL}; + + my $rs = $r->width; + $c->{width} = $rs if $rs > $c->{width}; + $c->{count}++ if $rs; + + push @{$c->{rows}} => $r; + } + } + + # Remove any empty columns we can + @$cols = grep {$_->{count} > 0 || $self->{+NO_COLLAPSE}->{$_->{idx}}} @$cols + if $self->{+COLLAPSE}; + + my $current = sum(map {$_->{width}} @$cols); + my $border = sum(BORDER_SIZE, PAD_SIZE, DIV_SIZE * @$cols); + my $total = $current + $border; + + if ($total > $self->{+MAX_WIDTH}) { + my $fair = ($self->{+MAX_WIDTH} - $border) / @$cols; + my $under = 0; + my @fix; + for my $c (@$cols) { + if ($c->{width} > $fair) { + push @fix => $c; + } + else { + $under += $c->{width}; + } + } + + # Recalculate fairness + $fair = int(($self->{+MAX_WIDTH} - $border - $under) / @fix); + + # Adjust over-long columns + $_->{width} = $fair for @fix; + } + + $self->{+_COLUMNS} = $cols; +} + +sub render { + my $self = shift; + + my $cols = $self->columns; + for my $col (@$cols) { + for my $cell (@{$col->{rows}}) { + $cell->reset; + } + } + my $width = sum(BORDER_SIZE, PAD_SIZE, DIV_SIZE * @$cols, map { $_->{width} } @$cols); + + #<<< NO-TIDY + my $border = '+' . join('+', map { '-' x ($_->{width} + CELL_PAD_SIZE) } @$cols) . '+'; + my $template = '|' . join('|', map { my $w = $_->{width} + CELL_PAD_SIZE; '%s' } @$cols) . '|'; + my $spacer = '|' . join('|', map { ' ' x ($_->{width} + CELL_PAD_SIZE) } @$cols) . '|'; + #>>> + + my @out = ($border); + my ($row, $split, $found) = (0, 0, 0); + while(1) { + my @row; + + my $is_spacer = 0; + + for my $col (@$cols) { + my $r = $col->{rows}->[$row]; + unless($r) { + push @row => ''; + next; + } + + my ($v, $vw); + + if ($r->isa('Term::Table::Cell')) { + my $lw = $r->border_left_width; + my $rw = $r->border_right_width; + $vw = $col->{width} - $lw - $rw; + $v = $r->break->next($vw); + } + elsif ($r->isa('Term::Table::CellStack')) { + ($v, $vw) = $r->break->next($col->{width}); + } + elsif ($r->isa('Term::Table::Spacer')) { + $is_spacer = 1; + } + + if ($is_spacer) { + last; + } + elsif (defined $v) { + $found++; + my $bcolor = $r->border_color || ''; + my $vcolor = $r->value_color || ''; + my $reset = $r->reset_color || ''; + + if (my $need = $vw - uni_length($v)) { + $v .= ' ' x $need; + } + + my $rt = "${reset}${bcolor}\%s${reset} ${vcolor}\%s${reset} ${bcolor}\%s${reset}"; + push @row => sprintf($rt, $r->border_left || '', $v, $r->border_right || ''); + } + else { + push @row => ' ' x ($col->{width} + 2); + } + } + + if (!grep {$_ && m/\S/} @row) { + last unless $found || $is_spacer; + + push @out => $border if $row == 0 && $self->{+SHOW_HEADER} && @{$self->{+HEADER}}; + push @out => $spacer if $split > 1 || $is_spacer; + + $row++; + $split = 0; + $found = 0; + + next; + } + + if ($split == 1 && @out > 1 && $out[-2] ne $border && $out[-2] ne $spacer) { + my $last = pop @out; + push @out => ($spacer, $last); + } + + push @out => sprintf($template, @row); + $split++; + } + + pop @out while @out && $out[-1] eq $spacer; + + unless (USE_GCS) { + for my $row (@out) { + next unless $row =~ m/[^\x00-\x7F]/; + unshift @out => "Unicode::GCString is not installed, table may not display all unicode characters properly"; + last; + } + } + + return (@out, $border); +} + +sub display { + my $self = shift; + my ($fh) = @_; + + my @parts = map "$_\n", $self->render; + + print $fh @parts if $fh; + print @parts; +} + +1; + +__END__ + + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table - Format a header and rows into a table + +=head1 DESCRIPTION + +This is used by some failing tests to provide diagnostics about what has gone +wrong. This module is able to generic format rows of data into tables. + +=head1 SYNOPSIS + + use Term::Table; + + my $table = Term::Table->new( + max_width => 80, # defaults to terminal size + collapse => 1, # do not show empty columns + header => [ 'name', 'age', 'hair color' ], + rows => [ + [ 'Fred Flinstone', 2000000, 'black' ], + [ 'Wilma Flinstone', 1999995, 'red' ], + ... + ], + ); + + say $_ for $table->render; + +This prints a table like this: + + +-----------------+---------+------------+ + | name | age | hair color | + +-----------------+---------+------------+ + | Fred Flinstone | 2000000 | black | + | Wilma Flinstone | 1999995 | red | + | ... | ... | ... | + +-----------------+---------+------------+ + +=head1 INTERFACE + + use Term::Table; + my $table = Term::Table->new(...); + +=head2 OPTIONS + +=over 4 + +=item header => [ ... ] + +If you want a header specify it here. This takes an arrayref with each columns +heading. + +=item rows => [ [...], [...], ... ] + +This should be an arrayref containing an arrayref per row. + +=item collapse => $bool + +Use this if you want to hide empty columns, that is any column that has no data +in any row. Having a header for the column will not effect collapse. + +=item max_width => $num + +Set the maximum width of the table, the table may not be this big, but it will +be no bigger. If none is specified it will attempt to find the width of your +terminal and use that, otherwise it falls back to the terminal width or C<80>. + +=item sanitize => $bool + +This will sanitize all the data in the table such that newlines, control +characters, and all whitespace except for ASCII 20 C<' '> are replaced with +escape sequences. This prevents newlines, tabs, and similar whitespace from +disrupting the table. + +B newlines are marked as '\n', but a newline is also inserted into the +data so that it typically displays in a way that is useful to humans. + +Example: + + my $field = "foo\nbar\nbaz\n"; + + print join "\n" => table( + sanitize => 1, + rows => [ + [$field, 'col2' ], + ['row2 col1', 'row2 col2'] + ] + ); + +Prints: + + +-----------------+-----------+ + | foo\n | col2 | + | bar\n | | + | baz\n | | + | | | + | row2 col1 | row2 col2 | + +-----------------+-----------+ + +So it marks the newlines by inserting the escape sequence, but it also shows +the data across as many lines as it would normally display. + +=item mark_tail => $bool + +This will replace the last whitespace character of any trailing whitespace with +its escape sequence. This makes it easier to notice trailing whitespace when +comparing values. + +=item show_header => $bool + +Set this to false to hide the header. This defaults to true if the header is +set, false if no header is provided. + +=item auto_columns => $bool + +Set this to true to automatically add columns that are not named in the header. +This defaults to false if a header is provided, and defaults to true when there +is no header. + +=item no_collapse => [ $col_num_a, $col_num_b, ... ] + +=item no_collapse => [ $col_name_a, $col_name_b, ... ] + +=item no_collapse => { $col_num_a => 1, $col_num_b => 1, ... } + +=item no_collapse => { $col_name_a => 1, $col_name_b => 1, ... } + +Specify (by number and/or name) columns that should not be removed when empty. +The 'name' form only works when a header is specified. There is currently no +protection to insure that names you specify are actually in the header, invalid +names are ignored, patches to fix this will be happily accepted. + +=back + +=head1 NOTE ON UNICODE/WIDE CHARACTERS + +Some unicode characters, such as C<婧> (C) are wider than others. These +will render just fine if you C as necessary, and +L is installed, however if the module is not installed there +will be anomalies in the table: + + +-----+-----+---+ + | a | b | c | + +-----+-----+---+ + | 婧 | x | y | + | x | y | z | + | x | 婧 | z | + +-----+-----+---+ + +=head1 SOURCE + +The source code repository for Term-Table can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/Term/Table/Cell.pm b/lib/Term/Table/Cell.pm new file mode 100644 index 0000000..c1bb21f --- /dev/null +++ b/lib/Term/Table/Cell.pm @@ -0,0 +1,147 @@ +package Term::Table::Cell; +use strict; +use warnings; + +our $VERSION = '0.012'; + +use Term::Table::LineBreak(); +use Term::Table::Util qw/uni_length/; + +use List::Util qw/sum/; + +use Term::Table::HashBase qw/value border_left border_right _break _widths border_color value_color reset_color/; + +my %CHAR_MAP = ( + # Special case, \n should render as \n, but also actually do the newline thing + "\n" => "\\n\n", + + "\a" => '\\a', + "\b" => '\\b', + "\e" => '\\e', + "\f" => '\\f', + "\r" => '\\r', + "\t" => '\\t', + " " => ' ', +); + +sub init { + my $self = shift; + + # Stringify + $self->{+VALUE} = defined $self->{+VALUE} ? "$self->{+VALUE}" : ''; +} + +sub char_id { + my $class = shift; + my ($char) = @_; + return "\\N{U+" . sprintf("\%X", ord($char)) . "}"; +} + +sub show_char { + my $class = shift; + my ($char, %props) = @_; + return $char if $props{no_newline} && $char eq "\n"; + return $CHAR_MAP{$char} || $class->char_id($char); +} + +sub sanitize { + my $self = shift; + $self->{+VALUE} =~ s/([\s\t\p{Zl}\p{C}\p{Zp}])/$self->show_char($1)/ge; # All whitespace except normal space +} + +sub mark_tail { + my $self = shift; + $self->{+VALUE} =~ s/([\s\t\p{Zl}\p{C}\p{Zp}])$/$1 eq ' ' ? $self->char_id($1) : $self->show_char($1, no_newline => 1)/se; +} + +sub value_width { + my $self = shift; + + my $w = $self->{+_WIDTHS} ||= {}; + return $w->{value} if defined $w->{value}; + + my @parts = split /(\n)/, $self->{+VALUE}; + + my $max = 0; + while (@parts) { + my $text = shift @parts; + my $sep = shift @parts || ''; + my $len = uni_length("$text"); + $max = $len if $len > $max; + } + + return $w->{value} = $max; +} + +sub border_left_width { + my $self = shift; + $self->{+_WIDTHS}->{left} ||= uni_length($self->{+BORDER_LEFT} || ''); +} + +sub border_right_width { + my $self = shift; + $self->{+_WIDTHS}->{right} ||= uni_length($self->{+BORDER_RIGHT} || ''); +} + +sub width { + my $self = shift; + $self->{+_WIDTHS}->{all} ||= sum(map { $self->$_ } qw/value_width border_left_width border_right_width/); +} + +sub break { + my $self = shift; + $self->{+_BREAK} ||= Term::Table::LineBreak->new(string => $self->{+VALUE}); +} + +sub reset { + my $self = shift; + delete $self->{+_BREAK}; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table::Cell - Representation of a cell in a table. + +=head1 DESCRIPTION + +This package is used to represent a cell in a table. + +=head1 SOURCE + +The source code repository for Term-Table can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/Term/Table/CellStack.pm b/lib/Term/Table/CellStack.pm new file mode 100644 index 0000000..032c896 --- /dev/null +++ b/lib/Term/Table/CellStack.pm @@ -0,0 +1,130 @@ +package Term::Table::CellStack; +use strict; +use warnings; + +our $VERSION = '0.012'; + +use Term::Table::HashBase qw/-cells -idx/; + +use List::Util qw/max/; + +sub init { + my $self = shift; + $self->{+CELLS} ||= []; +} + +sub add_cell { + my $self = shift; + push @{$self->{+CELLS}} => @_; +} + +sub add_cells { + my $self = shift; + push @{$self->{+CELLS}} => @_; +} + +sub sanitize { + my $self = shift; + $_->sanitize(@_) for @{$self->{+CELLS}}; +} + +sub mark_tail { + my $self = shift; + $_->mark_tail(@_) for @{$self->{+CELLS}}; +} + +my @proxy = qw{ + border_left border_right border_color value_color reset_color + border_left_width border_right_width +}; + +for my $meth (@proxy) { + no strict 'refs'; + *$meth = sub { + my $self = shift; + $self->{+CELLS}->[$self->{+IDX}]->$meth; + }; +} + +for my $meth (qw{value_width width}) { + no strict 'refs'; + *$meth = sub { + my $self = shift; + return max(map { $_->$meth } @{$self->{+CELLS}}); + }; +} + +sub next { + my $self = shift; + my ($cw) = @_; + + while ($self->{+IDX} < @{$self->{+CELLS}}) { + my $cell = $self->{+CELLS}->[$self->{+IDX}]; + + my $lw = $cell->border_left_width; + my $rw = $cell->border_right_width; + my $vw = $cw - $lw - $rw; + my $it = $cell->break->next($vw); + + return ($it, $vw) if $it; + $self->{+IDX}++; + } + + return; +} + +sub break { $_[0] } + +sub reset { + my $self = shift; + $self->{+IDX} = 0; + $_->reset for @{$self->{+CELLS}}; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table::CellStack - Combine several cells into one (vertical) + +=head1 DESCRIPTION + +This package is used to represent a merged-cell in a table (vertical). + +=head1 SOURCE + +The source code repository for Term-Table can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/Term/Table/HashBase.pm b/lib/Term/Table/HashBase.pm new file mode 100644 index 0000000..d789617 --- /dev/null +++ b/lib/Term/Table/HashBase.pm @@ -0,0 +1,289 @@ +package Term::Table::HashBase; +use strict; +use warnings; + +################################################################# +# # +# This is a generated file! Do not modify this file directly! # +# Use hashbase_inc.pl script to regenerate this file. # +# The script is part of the Object::HashBase distribution. # +# # +################################################################# + +{ + no warnings 'once'; + $Term::Table::HashBase::VERSION = '0.003'; + *Term::Table::HashBase::ATTR_SUBS = \%Object::HashBase::ATTR_SUBS; +} + + +require Carp; +{ + no warnings 'once'; + $Carp::Internal{+__PACKAGE__} = 1; +} + +BEGIN { + # these are not strictly equivalent, but for out use we don't care + # about order + *_isa = ($] >= 5.010 && require mro) ? \&mro::get_linear_isa : sub { + no strict 'refs'; + my @packages = ($_[0]); + my %seen; + for my $package (@packages) { + push @packages, grep !$seen{$_}++, @{"$package\::ISA"}; + } + return \@packages; + } +} + +my %STRIP = ( + '^' => 1, + '-' => 1, +); + +sub import { + my $class = shift; + my $into = caller; + + my $isa = _isa($into); + my $attr_subs = $Term::Table::HashBase::ATTR_SUBS{$into} ||= {}; + my %subs = ( + ($into->can('new') ? () : (new => \&_new)), + (map %{$Term::Table::HashBase::ATTR_SUBS{$_} || {}}, @{$isa}[1 .. $#$isa]), + ( + map { + my $p = substr($_, 0, 1); + my $x = $_; + substr($x, 0, 1) = '' if $STRIP{$p}; + my ($sub, $attr) = (uc $x, $x); + $sub => ($attr_subs->{$sub} = sub() { $attr }), + $attr => sub { $_[0]->{$attr} }, + $p eq '-' ? ("set_$attr" => sub { Carp::croak("'$attr' is read-only") }) + : $p eq '^' ? ("set_$attr" => sub { Carp::carp("set_$attr() is deprecated"); $_[0]->{$attr} = $_[1] }) + : ("set_$attr" => sub { $_[0]->{$attr} = $_[1] }), + } @_ + ), + ); + + no strict 'refs'; + *{"$into\::$_"} = $subs{$_} for keys %subs; +} + +sub _new { + my ($class, %params) = @_; + my $self = bless \%params, $class; + $self->init if $self->can('init'); + $self; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table::HashBase - Build hash based classes. + +=head1 SYNOPSIS + +A class: + + package My::Class; + use strict; + use warnings; + + # Generate 3 accessors + use Term::Table::HashBase qw/foo -bar ^baz/; + + # Chance to initialize defaults + sub init { + my $self = shift; # No other args + $self->{+FOO} ||= "foo"; + $self->{+BAR} ||= "bar"; + $self->{+BAZ} ||= "baz"; + } + + sub print { + print join ", " => map { $self->{$_} } FOO, BAR, BAZ; + } + +Subclass it + + package My::Subclass; + use strict; + use warnings; + + # Note, you should subclass before loading HashBase. + use base 'My::Class'; + use Term::Table::HashBase qw/bat/; + + sub init { + my $self = shift; + + # We get the constants from the base class for free. + $self->{+FOO} ||= 'SubFoo'; + $self->{+BAT} ||= 'bat'; + + $self->SUPER::init(); + } + +use it: + + package main; + use strict; + use warnings; + use My::Class; + + my $one = My::Class->new(foo => 'MyFoo', bar => 'MyBar'); + + # Accessors! + my $foo = $one->foo; # 'MyFoo' + my $bar = $one->bar; # 'MyBar' + my $baz = $one->baz; # Defaulted to: 'baz' + + # Setters! + $one->set_foo('A Foo'); + + #'-bar' means read-only, so the setter will throw an exception (but is defined). + $one->set_bar('A bar'); + + # '^baz' means deprecated setter, this will warn about the setter being + # deprecated. + $one->set_baz('A Baz'); + + $one->{+FOO} = 'xxx'; + +=head1 DESCRIPTION + +This package is used to generate classes based on hashrefs. Using this class +will give you a C method, as well as generating accessors you request. +Generated accessors will be getters, C setters will also be +generated for you. You also get constants for each accessor (all caps) which +return the key into the hash for that accessor. Single inheritance is also +supported. + +=head1 THIS IS A BUNDLED COPY OF HASHBASE + +This is a bundled copy of L. This file was generated using +the +C +script. + +=head1 METHODS + +=head2 PROVIDED BY HASH BASE + +=over 4 + +=item $it = $class->new(@VALUES) + +Create a new instance using key/value pairs. + +HashBase will not export C if there is already a C method in your +packages inheritance chain. + +B you just have to +declare it before loading L. + + package My::Package; + + # predeclare new() so that HashBase does not give us one. + sub new; + + use Term::Table::HashBase qw/foo bar baz/; + + # Now we define our own new method. + sub new { ... } + +This makes it so that HashBase sees that you have your own C method. +Alternatively you can define the method before loading HashBase instead of just +declaring it, but that scatters your use statements. + +=back + +=head2 HOOKS + +=over 4 + +=item $self->init() + +This gives you the chance to set some default values to your fields. The only +argument is C<$self> with its indexes already set from the constructor. + +=back + +=head1 ACCESSORS + +To generate accessors you list them when using the module: + + use Term::Table::HashBase qw/foo/; + +This will generate the following subs in your namespace: + +=over 4 + +=item foo() + +Getter, used to get the value of the C field. + +=item set_foo() + +Setter, used to set the value of the C field. + +=item FOO() + +Constant, returns the field C's key into the class hashref. Subclasses will +also get this function as a constant, not simply a method, that means it is +copied into the subclass namespace. + +The main reason for using these constants is to help avoid spelling mistakes +and similar typos. It will not help you if you forget to prefix the '+' though. + +=back + +=head1 SUBCLASSING + +You can subclass an existing HashBase class. + + use base 'Another::HashBase::Class'; + use Term::Table::HashBase qw/foo bar baz/; + +The base class is added to C<@ISA> for you, and all constants from base classes +are added to subclasses automatically. + +=head1 SOURCE + +The source code repository for HashBase can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/Term/Table/LineBreak.pm b/lib/Term/Table/LineBreak.pm new file mode 100644 index 0000000..4740fa9 --- /dev/null +++ b/lib/Term/Table/LineBreak.pm @@ -0,0 +1,143 @@ +package Term::Table::LineBreak; +use strict; +use warnings; + +our $VERSION = '0.012'; + +use Carp qw/croak/; +use Scalar::Util qw/blessed/; +use Term::Table::Util qw/uni_length/; + +use Term::Table::HashBase qw/string gcstring _len _parts idx/; + +sub init { + my $self = shift; + + croak "string is a required attribute" + unless defined $self->{+STRING}; +} + +sub columns { uni_length($_[0]->{+STRING}) } + +sub break { + my $self = shift; + my ($len) = @_; + $self->{+_LEN} = $len; + + $self->{+IDX} = 0; + my $str = $self->{+STRING} . ""; # Force stringification + + my @parts; + my @chars = split //, $str; + while (@chars) { + my $size = 0; + my $part = ''; + until ($size == $len) { + my $char = shift @chars; + $char = '' unless defined $char; + + my $l = uni_length("$char"); + last unless $l; + + last if $char eq "\n"; + + if ($size + $l > $len) { + unshift @chars => $char; + last; + } + + $size += $l; + $part .= $char; + } + + # If we stopped just before a newline, grab it + shift @chars if $size == $len && @chars && $chars[0] eq "\n"; + + until ($size == $len) { + $part .= ' '; + $size += 1; + } + push @parts => $part; + } + + $self->{+_PARTS} = \@parts; +} + +sub next { + my $self = shift; + + if (@_) { + my ($len) = @_; + $self->break($len) if !$self->{+_LEN} || $self->{+_LEN} != $len; + } + else { + croak "String has not yet been broken" + unless $self->{+_PARTS}; + } + + my $idx = $self->{+IDX}++; + my $parts = $self->{+_PARTS}; + + return undef if $idx >= @$parts; + return $parts->[$idx]; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table::LineBreak - Break up lines for use in tables. + +=head1 DESCRIPTION + +This is meant for internal use. This package takes long lines of text and +splits them so that they fit in table rows. + +=head1 SYNOPSIS + + use Term::Table::LineBreak; + + my $lb = Term::Table::LineBreak->new(string => $STRING); + + $lb->break($SIZE); + while (my $part = $lb->next) { + ... + } + +=head1 SOURCE + +The source code repository for Term-Table can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/Term/Table/Spacer.pm b/lib/Term/Table/Spacer.pm new file mode 100644 index 0000000..2c8f278 --- /dev/null +++ b/lib/Term/Table/Spacer.pm @@ -0,0 +1,15 @@ +package Term::Table::Spacer; +use strict; +use warnings; + +our $VERSION = '0.012'; + +sub new { bless {}, $_[0] } + +sub width { 1 } + +sub sanitize { } +sub mark_tail { } +sub reset { } + +1; diff --git a/lib/Term/Table/Util.pm b/lib/Term/Table/Util.pm new file mode 100644 index 0000000..1f0ba1b --- /dev/null +++ b/lib/Term/Table/Util.pm @@ -0,0 +1,195 @@ +package Term::Table::Util; +use strict; +use warnings; + +use Config qw/%Config/; + +our $VERSION = '0.012'; + +use Importer Importer => 'import'; +our @EXPORT_OK = qw/term_size USE_GCS USE_TERM_READKEY USE_TERM_SIZE_ANY uni_length/; + +sub DEFAULT_SIZE() { 80 } + +sub try(&) { + my $code = shift; + local ($@, $?, $!); + my $ok = eval { $code->(); 1 }; + my $err = $@; + return ($ok, $err); +} + +my ($tsa) = try { require Term::Size::Any; Term::Size::Any->import('chars') }; +my ($trk) = try { require Term::ReadKey }; +$trk &&= Term::ReadKey->can('GetTerminalSize'); + +if (!-t *STDOUT) { + *USE_TERM_READKEY = sub() { 0 }; + *USE_TERM_SIZE_ANY = sub() { 0 }; + *term_size = sub { + return $ENV{TABLE_TERM_SIZE} if $ENV{TABLE_TERM_SIZE}; + return DEFAULT_SIZE; + }; +} +elsif ($tsa) { + *USE_TERM_READKEY = sub() { 0 }; + *USE_TERM_SIZE_ANY = sub() { 1 }; + *_term_size = sub { + my $size = chars(\*STDOUT); + return DEFAULT_SIZE if !$size; + return DEFAULT_SIZE if $size < DEFAULT_SIZE; + return $size; + }; +} +elsif ($trk) { + *USE_TERM_READKEY = sub() { 1 }; + *USE_TERM_SIZE_ANY = sub() { 0 }; + *_term_size = sub { + my $total; + try { + my @warnings; + { + local $SIG{__WARN__} = sub { push @warnings => @_ }; + ($total) = Term::ReadKey::GetTerminalSize(*STDOUT); + } + @warnings = grep { $_ !~ m/Unable to get Terminal Size/ } @warnings; + warn @warnings if @warnings; + }; + return DEFAULT_SIZE if !$total; + return DEFAULT_SIZE if $total < DEFAULT_SIZE; + return $total; + }; +} +else { + *USE_TERM_READKEY = sub() { 0 }; + *USE_TERM_SIZE_ANY = sub() { 0 }; + *term_size = sub { + return $ENV{TABLE_TERM_SIZE} if $ENV{TABLE_TERM_SIZE}; + return DEFAULT_SIZE; + }; +} + +if (USE_TERM_READKEY() || USE_TERM_SIZE_ANY()) { + if (index($Config{sig_name}, 'WINCH') >= 0) { + my $changed = 0; + my $polled = -1; + $SIG{WINCH} = sub { $changed++ }; + + my $size; + *term_size = sub { + return $ENV{TABLE_TERM_SIZE} if $ENV{TABLE_TERM_SIZE}; + + unless ($changed == $polled) { + $polled = $changed; + $size = _term_size(); + } + + return $size; + } + } + else { + *term_size = sub { + return $ENV{TABLE_TERM_SIZE} if $ENV{TABLE_TERM_SIZE}; + _term_size(); + }; + } +} + +my ($gcs, $err) = try { require Unicode::GCString }; + +if ($gcs) { + *USE_GCS = sub() { 1 }; + *uni_length = sub { Unicode::GCString->new($_[0])->columns }; +} +else { + *USE_GCS = sub() { 0 }; + *uni_length = sub { length($_[0]) }; +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Term::Table::Util - Utilities for Term::Table. + +=head1 DESCRIPTION + +This package exports some tools used by Term::Table. + +=head1 EXPORTS + +=head2 CONSTANTS + +=over 4 + +=item $bool = USE_GCS + +True if L is installed. + +=item $bool = USE_TERM_READKEY + +True if L is installed. + +=back + +=head2 UTILITIES + +=over 4 + +=item $width = term_size() + +Get the width of the terminal. + +If the C<$TABLE_TERM_SIZE> environment variable is set then that value will be +returned. + +This will default to 80 if there is no good way to get the size, or if the size +is unreasonably small. + +If L is installed it will be used. + +=item $width = uni_length($string) + +Get the width (in columns) of the specified string. When L +is installed this will work on unicode strings, otherwise it will just use +C. + +=back + +=head1 SOURCE + +The source code repository for Term-Table can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright 2016 Chad Granum Eexodist@cpan.orgE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/t/HashBase.t b/t/HashBase.t new file mode 100644 index 0000000..9223773 --- /dev/null +++ b/t/HashBase.t @@ -0,0 +1,157 @@ +use strict; +use warnings; + +use Test::More; + + +sub warnings(&) { + my $code = shift; + my @warnings; + local $SIG{__WARN__} = sub { push @warnings => @_ }; + $code->(); + return \@warnings; +} + +sub exception(&) { + my $code = shift; + local ($@, $!, $SIG{__DIE__}); + my $ok = eval { $code->(); 1 }; + my $error = $@ || 'SQUASHED ERROR'; + return $ok ? undef : $error; +} + +BEGIN { + $INC{'Object/HashBase/Test/HBase.pm'} = __FILE__; + + package + main::HBase; + use Term::Table::HashBase qw/foo bar baz/; + + main::is(FOO, 'foo', "FOO CONSTANT"); + main::is(BAR, 'bar', "BAR CONSTANT"); + main::is(BAZ, 'baz', "BAZ CONSTANT"); +} + +BEGIN { + package + main::HBaseSub; + use base 'main::HBase'; + use Term::Table::HashBase qw/apple pear/; + + main::is(FOO, 'foo', "FOO CONSTANT"); + main::is(BAR, 'bar', "BAR CONSTANT"); + main::is(BAZ, 'baz', "BAZ CONSTANT"); + main::is(APPLE, 'apple', "APPLE CONSTANT"); + main::is(PEAR, 'pear', "PEAR CONSTANT"); +} + +my $one = main::HBase->new(foo => 'a', bar => 'b', baz => 'c'); +is($one->foo, 'a', "Accessor"); +is($one->bar, 'b', "Accessor"); +is($one->baz, 'c', "Accessor"); +$one->set_foo('x'); +is($one->foo, 'x', "Accessor set"); +$one->set_foo(undef); + +is_deeply( + $one, + { + foo => undef, + bar => 'b', + baz => 'c', + }, + 'hash' +); + +BEGIN { + package + main::Const::Test; + use Term::Table::HashBase qw/foo/; + + sub do_it { + if (FOO()) { + return 'const'; + } + return 'not const' + } +} + +my $pkg = 'main::Const::Test'; +is($pkg->do_it, 'const', "worked as expected"); +{ + local $SIG{__WARN__} = sub { }; + *main::Const::Test::FOO = sub { 0 }; +} +ok(!$pkg->FOO, "overrode const sub"); +is($pkg->do_it, 'const', "worked as expected, const was constant"); + +BEGIN { + $INC{'Object/HashBase/Test/HBase/Wrapped.pm'} = __FILE__; + + package + main::HBase::Wrapped; + use Term::Table::HashBase qw/foo bar/; + + my $foo = __PACKAGE__->can('foo'); + no warnings 'redefine'; + *foo = sub { + my $self = shift; + $self->set_bar(1); + $self->$foo(@_); + }; +} + +BEGIN { + $INC{'Object/HashBase/Test/HBase/Wrapped/Inherit.pm'} = __FILE__; + + package + main::HBase::Wrapped::Inherit; + use base 'main::HBase::Wrapped'; + use Term::Table::HashBase; +} + +my $o = main::HBase::Wrapped::Inherit->new(foo => 1); +my $foo = $o->foo; +is($o->bar, 1, 'parent attribute sub not overridden'); + +{ + package + Foo; + + sub new; + + use Term::Table::HashBase qw/foo bar baz/; + + sub new { 'foo' }; +} + +is(Foo->new, 'foo', "Did not override existing 'new' method"); + +BEGIN { + $INC{'Object/HashBase/Test/HBase2.pm'} = __FILE__; + + package + main::HBase2; + use Term::Table::HashBase qw/foo -bar ^baz/; + + main::is(FOO, 'foo', "FOO CONSTANT"); + main::is(BAR, 'bar', "BAR CONSTANT"); + main::is(BAZ, 'baz', "BAZ CONSTANT"); +} + +my $ro = main::HBase2->new(foo => 'foo', bar => 'bar', baz => 'baz'); +is($ro->foo, 'foo', "got foo"); +is($ro->bar, 'bar', "got bar"); +is($ro->baz, 'baz', "got baz"); + +is($ro->set_foo('xxx'), 'xxx', "Can set foo"); +is($ro->foo, 'xxx', "got foo"); + +like(exception { $ro->set_bar('xxx') }, qr/'bar' is read-only/, "Cannot set bar"); + +my $warnings = warnings { is($ro->set_baz('xxx'), 'xxx', 'set baz') }; +like($warnings->[0], qr/set_baz\(\) is deprecated/, "Deprecation warning"); + +done_testing; + +1; diff --git a/t/Table.t b/t/Table.t new file mode 100644 index 0000000..f78d580 --- /dev/null +++ b/t/Table.t @@ -0,0 +1,303 @@ +use Term::Table; +use Term::Table::Util qw/USE_GCS/; + +use Test2::Tools::Tiny; + +use utf8; +use strict; +use warnings; + +use Test2::API qw/test2_stack/; +test2_stack->top->format->encoding('utf8'); + +sub table { Term::Table->new(@_)->render } + +tests unicode_display_width => sub { + my $wide = "foo bar baz 婧"; + + my $have_gcstring = eval { require Unicode::GCString; 1 }; + + tests no_unicode_linebreak => sub { + my @table = table('header' => [ 'a', 'b'], 'rows' => [[ '婧', '߃' ]]); + + is( + $table[0], + "Unicode::GCString is not installed, table may not display all unicode characters properly", + "got unicode note" + ); + } unless USE_GCS; + + tests with_unicode_linebreak => sub { + my @table = table( + 'header' => [ 'a', 'b'], + 'rows' => [[ 'a婧b', '߃' ]], + 'max_width' => 80, + ); + is_deeply( + \@table, + [ + '+------+---+', + '| a | b |', + '+------+---+', + '| a婧b | ߃ |', + '+------+---+', + ], + "Support for unicode characters that use multiple columns" + ); + } if USE_GCS; +}; + +tests width => sub { + my @table = table( + max_width => 40, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc ddddddddddddddddddddddddddddd/ ], + [ qw/AAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCCCCCCCCCC DDDDDDDDDDDDDDDDDDDDDDDDDDDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+-------+-------+-------+-------+', + '| a | b | c | d |', + '+-------+-------+-------+-------+', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | b | ccc | ddddd |', + '| a | | | dddd |', + '| | | | |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | B | CCC | DDDDD |', + '| A | | | DDDD |', + '+-------+-------+-------+-------+', + ], + "Basic table, small width" + ); + + @table = table( + max_width => 60, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc ddddddddddddddddddddddddddddd/ ], + [ qw/AAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCCCCCCCCCC DDDDDDDDDDDDDDDDDDDDDDDDDDDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------------+------------+------------+------------+', + '| a | b | c | d |', + '+------------+------------+------------+------------+', + '| aaaaaaaaaa | bbbbbbbbbb | cccccccccc | dddddddddd |', + '| aaaaaaaaaa | bbbbbbbbbb | cccccccccc | dddddddddd |', + '| aaaaaa | b | ccc | ddddddddd |', + '| | | | |', + '| AAAAAAAAAA | BBBBBBBBBB | CCCCCCCCCC | DDDDDDDDDD |', + '| AAAAAAAAAA | BBBBBBBBBB | CCCCCCCCCC | DDDDDDDDDD |', + '| AAAAAA | B | CCC | DDDDDDDDD |', + '+------------+------------+------------+------------+', + ], + "Basic table, bigger width" + ); + + @table = table( + max_width => 60, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaa bbbb cccc dddd/ ], + [ qw/AAAA BBBB CCCC DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+------+------+', + '| a | b | c | d |', + '+------+------+------+------+', + '| aaaa | bbbb | cccc | dddd |', + '| AAAA | BBBB | CCCC | DDDD |', + '+------+------+------+------+', + ], + "Short table, well under minimum", + ); +}; + +tests collapse => sub { + my @table = table( + max_width => 60, + collapse => 1, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaa bbbb/, undef, qw/dddd/ ], + [ qw/AAAA BBBB/, '', qw/DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+------+', + '| a | b | d |', + '+------+------+------+', + '| aaaa | bbbb | dddd |', + '| AAAA | BBBB | DDDD |', + '+------+------+------+', + ], + "Table collapsed", + ); + + @table = table( + max_width => 60, + collapse => 0, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaa bbbb/, undef, qw/dddd/ ], + [ qw/AAAA BBBB/, '', qw/DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+---+------+', + '| a | b | c | d |', + '+------+------+---+------+', + '| aaaa | bbbb | | dddd |', + '| AAAA | BBBB | | DDDD |', + '+------+------+---+------+', + ], + "Table not collapsed", + ); + + @table = table( + max_width => 60, + collapse => 1, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaa bbbb/, undef, qw/dddd/ ], + [ qw/AAAA BBBB/, 0, qw/DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+---+------+', + '| a | b | c | d |', + '+------+------+---+------+', + '| aaaa | bbbb | | dddd |', + '| AAAA | BBBB | 0 | DDDD |', + '+------+------+---+------+', + ], + "'0' value does not cause collapse", + ); + +}; + +tests header => sub { + my @table = table( + max_width => 60, + header => [ 'a', 'b', 'c', 'd' ], + rows => [ + [ qw/aaaa bbbb cccc dddd/ ], + [ qw/AAAA BBBB CCCC DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+------+------+', + '| a | b | c | d |', + '+------+------+------+------+', + '| aaaa | bbbb | cccc | dddd |', + '| AAAA | BBBB | CCCC | DDDD |', + '+------+------+------+------+', + ], + "Table with header", + ); +}; + +tests no_header => sub { + my @table = table( + max_width => 60, + rows => [ + [ qw/aaaa bbbb cccc dddd/ ], + [ qw/AAAA BBBB CCCC DDDD/ ], + ], + ); + + is_deeply( + \@table, + [ + '+------+------+------+------+', + '| aaaa | bbbb | cccc | dddd |', + '| AAAA | BBBB | CCCC | DDDD |', + '+------+------+------+------+', + ], + "Table without header", + ); +}; + +tests sanitize => sub { + my @table = table( + max_width => 60, + sanitize => 1, + header => [ 'data1' ], + rows => [["a\t\n\r\b\a        

 ‌\N{U+000B}bф"]], + ); + + my $have_gcstring = eval { require Unicode::GCString; 1 } || 0; + + is_deeply( + \@table, + [ + ( $have_gcstring + ? () + : ("Unicode::GCString is not installed, table may not display all unicode characters properly") + ), + '+---------------------------------------------------+', + '| data1 |', + '+---------------------------------------------------+', + '| a\t\n |', + '| \r\b\a\N{U+A0}\N{U+1680}\N{U+2000}\N{U+2001}\N{U+ |', + '| 2002}\N{U+2003}\N{U+2004}\N{U+2008}\N{U+2028}\N{U |', + '| +2029}\N{U+3000}\N{U+200C}\N{U+FEFF}\N{U+B}bф |', + '+---------------------------------------------------+', + ], + "Sanitized data" + ); +}; + +tests mark_tail => sub { + my @table = table( + max_width => 60, + mark_tail => 1, + header => [ 'data1', 'data2' ], + rows => [[" abc def ", " abc def \t"]], + ); + + is_deeply( + \@table, + [ + '+----------------------+----------------+', + '| data1 | data2 |', + '+----------------------+----------------+', + '| abc def \N{U+20} | abc def \t |', + '+----------------------+----------------+', + ], + "Sanitized data" + ); + +}; + +done_testing; diff --git a/t/Table/Cell.t b/t/Table/Cell.t new file mode 100644 index 0000000..6433da3 --- /dev/null +++ b/t/Table/Cell.t @@ -0,0 +1,44 @@ +use Test2::Tools::Tiny; +use Term::Table::Cell; +use strict; +use warnings; +use utf8; + +use Test2::API qw/test2_stack/; +test2_stack->top->format->encoding('utf8'); + +tests sanitization => sub { + my $unsanitary = <<" EOT"; +This string +has vertical space +including        

 ‌\N{U+000B}unicode stuff +and some non-whitespace ones: 婧 ʶ ๖ + EOT + my $sanitary = 'This string\nhas vertical space\nincluding\N{U+A0}\N{U+1680}\N{U+2000}\N{U+2001}\N{U+2002}\N{U+2003}\N{U+2004}\N{U+2008}\N{U+2028}\N{U+2029}\N{U+3000}\N{U+200C}\N{U+FEFF}\N{U+B}unicode stuff\nand some non-whitespace ones: 婧 ʶ ๖\n'; + $sanitary =~ s/\\n/\\n\n/g; + + local *show_char = sub { Term::Table::Cell->show_char(@_) }; + + # Common control characters + is(show_char("\a"), '\a', "translated bell"); + is(show_char("\b"), '\b', "translated backspace"); + is(show_char("\e"), '\e', "translated escape"); + is(show_char("\f"), '\f', "translated formfeed"); + is(show_char("\n"), "\\n\n", "translated newline"); + is(show_char("\r"), '\r', "translated return"); + is(show_char("\t"), '\t', "translated tab"); + is(show_char(" "), ' ', "plain space is not translated"); + + # unicodes + is(show_char("婧"), '\N{U+5A67}', "translated unicode 婧 (U+5A67)"); + is(show_char("ʶ"), '\N{U+2B6}', "translated unicode ʶ (U+2B6)"); + is(show_char("߃"), '\N{U+7C3}', "translated unicode ߃ (U+7C3)"); + is(show_char("๖"), '\N{U+E56}', "translated unicode ๖ (U+E56)"); + + my $cell = Term::Table::Cell->new(value => $unsanitary); + $cell->sanitize; + + is($cell->value, $sanitary, "Sanitized string"); +}; + +done_testing; diff --git a/t/Table/CellStack.t b/t/Table/CellStack.t new file mode 100644 index 0000000..6b090ee --- /dev/null +++ b/t/Table/CellStack.t @@ -0,0 +1,77 @@ +use Term::Table; +use Term::Table::Util; +use Test2::Tools::Tiny; + +use utf8; +use strict; +use warnings; + +use Term::Table::CellStack; + +sub table { Term::Table->new(@_)->render } + +my @table = table( + max_width => 40, + header => ['a', 'b', 'c', 'd'], + rows => [ + [qw/aaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc ddddddddddddddddddddddddddddd/], + [ + Term::Table::CellStack->new(cells => [ + Term::Table::Cell->new(border_left => '>', border_right => '<', value => 'aaa'), + Term::Table::Cell->new(value => 'bbb'), + Term::Table::Cell->new(border_left => '>', border_right => '<', value => 'ccc'), + ]), + Term::Table::CellStack->new(cells => [ + Term::Table::Cell->new(border_left => '>', border_right => '<', value => 'aaaaaaaaaaaaaaaaaaaaa'), + Term::Table::Cell->new(value => 'bbbbbbbbbbbbbbbbbbbb'), + Term::Table::Cell->new(border_left => '>', border_right => '<', value => 'ccccccccccccccccccccc'), + ]), + ], + [qw/AAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCCCCCCCCCCCCCC DDDDDDDDDDDDDDDDDDDDDDDDDDDDD/], + ], +); + +is_deeply( + \@table, + [ + '+-------+-------+-------+-------+', + '| a | b | c | d |', + '+-------+-------+-------+-------+', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | bbbbb | ccccc | ddddd |', + '| aaaaa | b | ccc | ddddd |', + '| a | | | dddd |', + '| | | | |', + '|> aaa <|> aaa <| | |', + '| bbb |> aaa <| | |', + '|> ccc <|> aaa <| | |', + '| |> aaa <| | |', + '| |> aaa <| | |', + '| |> aaa <| | |', + '| |> aaa <| | |', + '| | bbbbb | | |', + '| | bbbbb | | |', + '| | bbbbb | | |', + '| | bbbbb | | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| |> ccc <| | |', + '| | | | |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | BBBBB | CCCCC | DDDDD |', + '| AAAAA | B | CCC | DDDDD |', + '| A | | | DDDD |', + '+-------+-------+-------+-------+', + ], + "Basic table, small width" +); + +done_testing; diff --git a/t/Table/LineBreak.t b/t/Table/LineBreak.t new file mode 100644 index 0000000..74eadd9 --- /dev/null +++ b/t/Table/LineBreak.t @@ -0,0 +1,79 @@ +use Test2::Tools::Tiny; +use Term::Table::LineBreak; +use strict; +use warnings; +use utf8; + +use Test2::API qw/test2_stack/; +test2_stack->top->format->encoding('utf8'); + +tests with_unicode_linebreak => sub { + my $one = Term::Table::LineBreak->new(string => 'aaaa婧bbbb'); + $one->break(3); + is_deeply( + [ map { $one->next } 1 .. 5 ], + [ + 'aaa', + 'a婧', + 'bbb', + 'b ', + undef + ], + "Got all parts" + ); + + $one = Term::Table::LineBreak->new(string => 'a婧bb'); + $one->break(2); + is_deeply( + [ map { $one->next } 1 .. 4 ], + [ + 'a ', + '婧', + 'bb', + undef + ], + "Padded the problem" + ); + +} if $INC{'Unicode/LineBreak.pm'}; + +tests without_unicode_linebreak => sub { + my @parts; + { + local %INC = %INC; + delete $INC{'Unicode/GCString.pm'}; + my $one = Term::Table::LineBreak->new(string => 'aaaa婧bbbb'); + $one->break(3); + @parts = map { $one->next } 1 .. 5; + } + + todo "Can't handle unicode properly without Unicode::GCString" => sub { + is_deeply( + \@parts, + [ + 'aaa', + 'a婧', + 'bbb', + 'b ', + undef + ], + "Got all parts" + ); + }; + + my $one = Term::Table::LineBreak->new(string => 'aaabbbx'); + $one->break(2); + is_deeply( + [ map { $one->next } 1 .. 5 ], + [ + 'aa', + 'ab', + 'bb', + 'x ', + undef + ], + "Padded the problem" + ); +}; + +done_testing; diff --git a/t/bad_blank_line.t b/t/bad_blank_line.t new file mode 100644 index 0000000..dc87dd4 --- /dev/null +++ b/t/bad_blank_line.t @@ -0,0 +1,66 @@ +use Test2::Tools::Tiny; +use strict; +use warnings; + +use Term::Table; +use Term::Table::Cell; + +# This example was produced from the end result of another process, the end +# result is reproduced here in shortcuts: + +chomp(my $inner = < ... <| 26, 30 | +| | | | a | 27 | +| | | | b | 28 | +| | | | c | 29 | ++------+-----+-----+-------+--------+ +EOT + +my $rows = [[ + '', + '', + bless({'value' => $inner}, 'Term::Table::Cell'), + bless({'value' => 'eq'}, 'Term::Table::Cell'), + bless({'value' => ""}, 'Term::Table::Cell'), + '', + bless({'value' => '67'}, 'Term::Table::Cell'), + '' + ], +]; + +my $table = Term::Table->new( + collapse => 1, + sanitize => 1, + mark_tail => 1, + show_header => 1, + term_size => 80, + header => [qw/PATH LINES GOT OP CHECK * LINES NOTES/], + no_collapse => [qw/GOT CHECK/], + rows => $rows, +); + +is_deeply( + [ $table->render ], + [ + '+-----------------------------------------+----+-------+-------+', + '| GOT | OP | CHECK | LINES |', + '+-----------------------------------------+----+-------+-------+', + '| +------+-----+-----+-------+--------+\n | eq | | 67 |', + '| | PATH | GOT | OP | CHECK | LINES |\n | | | |', + '| +------+-----+-----+-------+--------+\n | | | |', + '| | [0] | x | ANY |> ... <| 26, 30 |\n | | | |', + '| | | | | a | 27 |\n | | | |', + '| | | | | b | 28 |\n | | | |', + '| | | | | c | 29 |\n | | | |', + '| +------+-----+-----+-------+--------+ | | | |', + '+-----------------------------------------+----+-------+-------+', + ], + "Table looks right" +); + +print map { "$_\n" } $table->render; + +done_testing; diff --git a/t/honor_env_in_non_tty.t b/t/honor_env_in_non_tty.t new file mode 100644 index 0000000..45feb8e --- /dev/null +++ b/t/honor_env_in_non_tty.t @@ -0,0 +1,28 @@ +use Test2::Tools::Tiny; +use strict; +use warnings; + +BEGIN { + my $out = ""; + local *STDOUT; + open(STDOUT, '>', \$out) or die "Could not open a temp STDOUT: $!"; + ok(!-t STDOUT, "STDOUT is not a term"); + + require Term::Table::Util; + Term::Table::Util->import(qw/term_size USE_TERM_READKEY USE_TERM_SIZE_ANY/); +} + +ok(!USE_TERM_READKEY, "Not using Term::Readkey without a term"); +ok(!USE_TERM_SIZE_ANY, "Not using Term::Size::Any without a term"); + +{ + local $ENV{TABLE_TERM_SIZE}; + is(term_size, Term::Table::Util->DEFAULT_SIZE, "Get default size without the var"); +} + +{ + local $ENV{TABLE_TERM_SIZE} = 1234; + is(term_size, 1234, "Used the size in the env var"); +} + +done_testing;