File Coverage

blib/lib/Text/UnicodeBox.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Text::UnicodeBox;
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Text::UnicodeBox - Text box drawing using the Unicode box symbols
8              
9             =head1 SYNOPSIS
10              
11             use Text::UnicodeBox;
12             use Text::UnicodeBox::Control qw(:all);
13            
14             my $box = Text::UnicodeBox->new();
15             $box->add_line(
16             BOX_START( style => 'double', top => 'double', bottom => 'double' ), ' ', BOX_END(),
17             ' ',
18             BOX_START( style => 'heavy', top => 'heavy', bottom => 'heavy' ), ' ', BOX_END()
19             );
20             print $box->render();
21              
22             # Renders:
23             # ╔═══╗ ┏━━━┓
24             # â•‘ â•‘ ┃ ┃
25             # ╚═══╝ ┗━━━┛
26              
27             =head1 DESCRIPTION
28              
29             Text::UnicodeBox is a low level box drawing interface. You'll most likely want to use one of the higher level modules such as L<Text::UnicodeBox::Table>.
30              
31             The unicode box symbol table (L<http://en.wikipedia.org/wiki/Box-drawing_character>) is a fairly robust set of symbols that allow you to draw lines and boxes with monospaced fonts. This module allows you to focus on the content of the boxes you need to draw and mostly ignore how to draw a good looking box with proper connections between all the lines.
32              
33             The low level approach is line-based. A box object is created, C<add_line> is called for each line of content you'd like to render, and C<render> is called to complete the box.
34              
35             Output is built up over time, which allows you to stream the output rather then buffering it and printing it in one go.
36              
37             =cut
38              
39 5     5   168632 use Moose;
  0            
  0            
40              
41             use Text::UnicodeBox::Control qw(:all);
42             use Text::UnicodeBox::Text qw(:all);
43             use Text::UnicodeBox::Utility qw(normalize_box_character_parameters);
44             use Scalar::Util qw(blessed);
45              
46             has 'buffer_ref' => ( is => 'rw', default => sub { my $buffer = ''; return \$buffer } );
47             has 'last_line' => ( is => 'rw' );
48             has 'whitespace_character' => ( is => 'ro', default => ' ' );
49             has 'fetch_box_character' => ( is => 'rw' );
50              
51             our $VERSION = 0.03;
52              
53             =head1 METHODS
54              
55             =head2 new (%params)
56              
57             Create a new instance. Provide arguments as a list. Valid arguments are:
58              
59             =over 4
60              
61             =item whitespace_character (default: ' ')
62              
63             When the box renderer needs to pad the output of the interstitial lines of output, this character will be used. Defaults to a simple space.
64              
65             =item fetch_box_character
66              
67             Provide a subroutine which will be used instead of the L<Text::UnicodeBox::Utility/fetch_box_character>. This allows the user granular control over what symbols will be used for box drawing. The subroutine will be called with a hash with any or all of the following keys: 'left', 'right', up', 'down', 'vertical' or 'horizontal'. The value of each will be either '1' (default style), 'light', 'heavy', 'single' or 'double'.
68              
69             Return a single width character or return undefined and a '?' will be used for rendering.
70              
71             =back
72              
73             =head2 buffer
74              
75             Return the current buffer of rendered text.
76              
77             =cut
78              
79             sub buffer {
80             my $self = shift;
81             return ${ $self->buffer_ref };
82             }
83              
84             =head2 add_line (@parts)
85              
86             Pass a list of parts for a rendered line of output. You may pass either a string, a L<Text::UnicodeBox::Control> or a L<Text::UnicodeBox::Text> object. Strings will be transformed into the latter. The line will be rendered to the buffer.
87              
88             =cut
89              
90             sub add_line {
91             my $self = shift;
92             my @parts;
93              
94             # Read off each arg, validate, then push onto @parts as objects
95             foreach my $part (@_) {
96             if (ref $part && blessed $part && ($part->isa('Text::UnicodeBox::Control') || $part->isa('Text::UnicodeBox::Text'))) {
97             push @parts, $part;
98             }
99             elsif (ref $part) {
100             die "add_line() takes only strings or Text::UnicodeBox:: objects as arguments";
101             }
102             else {
103             push @parts, BOX_STRING($part);
104             }
105             }
106              
107             my %current_line = (
108             parts => \@parts,
109             parts_at_position => {},
110             );
111              
112             # Generate this line as text
113             my $line = '';
114             {
115             my $position = 0;
116             my %context;
117             foreach my $part (@parts) {
118             $current_line{parts_at_position}{$position} = $part;
119             $line .= $part->to_string(\%context, $self);
120             $position += $part->can('length') ? $part->length : 1;
121             }
122             $line .= "\n";
123             $current_line{final_position} = $position;
124             }
125              
126             ## Generate the top of the box if needed
127              
128             my $box_border_line;
129             if (grep { $_->can('top') && $_->top } @parts) {
130             $box_border_line = $self->_generate_box_border_line(\%current_line);
131             }
132             elsif ($self->last_line && grep { $_->can('bottom') && $_->bottom } @{ $self->last_line->{parts} }) {
133             $box_border_line = $self->_generate_box_border_line(\%current_line);
134             }
135              
136             # Store this for later reference
137             $self->last_line(\%current_line);
138              
139             # Add lines to the buffer ref
140             my $buffer_ref = $self->buffer_ref;
141             $$buffer_ref .= $box_border_line if defined $box_border_line;
142             $$buffer_ref .= $line;
143             }
144              
145             =head2 render
146              
147             Complete the rendering of the box, drawing any final lines needed to close up the drawing.
148              
149             Returns the buffer
150              
151             =cut
152              
153             sub render {
154             my $self = shift;
155              
156             my @box_bottoms = grep { $_->can('bottom') && $_->bottom } @{ $self->last_line->{parts} };
157             if (@box_bottoms) {
158             my $box_border_line = $self->_generate_box_border_line();
159             my $buffer_ref = $self->buffer_ref;
160             $$buffer_ref .= $box_border_line;
161             }
162              
163             return $self->buffer();
164             }
165              
166             sub _find_part_at_position {
167             my ($line_details, $position) = @_;
168             return if $position >= $line_details->{final_position};
169             while ($position >= 0) {
170             if (my $return = $line_details->{parts_at_position}{$position}) {
171             return $return;
172             }
173             $position--;
174             }
175             return;
176             }
177              
178             sub _generate_box_border_line {
179             my ($self, $current_line) = @_;
180             my ($below_box_style, $above_box_style);
181              
182             # Find the largest final_position value
183             my $final_position = $current_line ? $current_line->{final_position} : 0;
184             $final_position = $self->last_line->{final_position}
185             if $self->last_line && $self->last_line->{final_position} > $final_position;
186              
187             my $line = '';
188             foreach my $position (0..$final_position - 1) {
189             my ($above_part, $below_part);
190             $above_part = _find_part_at_position($self->last_line, $position) if $self->last_line;
191             $below_part = _find_part_at_position($current_line, $position) if $current_line;
192              
193             my %symbol;
194             # First, let the above part specify styling
195             if ($above_part && $above_part->isa('Text::UnicodeBox::Control')) {
196             $symbol{up} = $above_part->style || 'light';
197             if ($above_part->position eq 'start' && $above_part->bottom) {
198             $above_box_style = $above_part->bottom;
199             $symbol{right} = $above_box_style;
200             }
201             elsif ($above_part->position eq 'end') {
202             $symbol{left} = $above_box_style;
203             $above_box_style = undef;
204             }
205             elsif ($above_part->position eq 'rule') {
206             $symbol{left} = $symbol{right} = $above_box_style;
207             }
208             }
209             elsif ($above_part && $above_part->isa('Text::UnicodeBox::Text') && $above_box_style) {
210             $symbol{left} = $symbol{right} = $above_box_style;
211             }
212              
213             # Next, let the below part override
214             if ($below_part && $below_part->isa('Text::UnicodeBox::Control')) {
215             $symbol{down} = $below_part->style || 'light';
216             if ($below_part->position eq 'start' && $below_part->top) {
217             $below_box_style = $below_part->top;
218             $symbol{right} = $below_box_style if $below_box_style;
219             }
220             elsif ($below_part->position eq 'end') {
221             $symbol{left} = $below_box_style if $below_box_style;
222             $below_box_style = undef;
223             }
224             elsif ($below_part->position eq 'rule') {
225             $symbol{left} = $symbol{right} = $below_box_style if $below_box_style;
226             }
227             }
228             elsif ($below_part && $below_part->isa('Text::UnicodeBox::Text') && $below_box_style) {
229             $symbol{left} = $symbol{right} = $below_box_style;
230             }
231             if (! keys %symbol) {
232             $symbol{horizontal} = $below_box_style ? $below_box_style : $above_box_style ? $above_box_style : undef;
233             delete $symbol{horizontal} unless defined $symbol{horizontal};
234             }
235              
236             # Find the character and add it to the line
237             my $char;
238             if (! keys %symbol) {
239             $char = $self->whitespace_character();
240             }
241             else {
242             $char = $self->_fetch_box_character(%symbol);
243             }
244             $char = '?' unless defined $char;
245             $line .= $char;
246             }
247              
248             $line .= "\n";
249              
250             return $line;
251             }
252              
253             sub _fetch_box_character {
254             my ($self, %symbol) = @_;
255             my $cache_key = join ';', map { "$_=$symbol{$_}" } sort keys %symbol;
256             if (exists $self->{_fetch_box_character_cache}{$cache_key}) {
257             return $self->{_fetch_box_character_cache}{$cache_key};
258             }
259             my $char;
260             if ($self->fetch_box_character) {
261             $char = $self->fetch_box_character->(
262             normalize_box_character_parameters(%symbol)
263             );
264             }
265             else {
266             $char = Text::UnicodeBox::Utility::fetch_box_character(%symbol);
267             }
268             $self->{_fetch_box_character_cache}{$cache_key} = $char;
269             return $char;
270             }
271              
272             =head1 DEVELOPMENT
273              
274             This module is being developed via a git repository publicly avaiable at http://github.com/ewaters/Text-UnicodeBox. I encourage anyone who is interested to fork my code and contribute bug fixes or new features, or just have fun and be creative.
275              
276             =head1 COPYRIGHT
277              
278             Copyright (c) 2012 Eric Waters and Shutterstock Images (http://shutterstock.com). All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
279              
280             The full text of the license can be found in the LICENSE file included with this module.
281              
282             =head1 AUTHOR
283              
284             Eric Waters <ewaters@gmail.com>
285              
286             =cut
287              
288             1;