File Coverage

blib/lib/Text/KnuthPlass.pm
Criterion Covered Total %
statement 124 173 71.6
branch 15 40 37.5
condition 8 30 26.6
subroutine 27 35 77.1
pod 7 7 100.0
total 181 285 63.5


line stmt bran cond sub pod time code
1             package Text::KnuthPlass;
2             require XSLoader;
3 3     3   141575 use constant DEBUG => 0;
  3         15  
  3         278  
4 3     3   20 use warnings;
  3         6  
  3         78  
5 3     3   17 use strict;
  3         5  
  3         225  
6              
7             our $VERSION = '1.05'; # VERSION
8             my $LAST_UPDATE = '1.05'; # manually update whenever file is edited
9              
10             eval { XSLoader::load("Text::KnuthPlass", $VERSION); } or die $@;
11             # Or else there's a Perl version
12 3     3   2028 use Data::Dumper;
  3         21359  
  3         222  
13              
14             package Text::KnuthPlass::Element;
15 3     3   23 use base 'Class::Accessor';
  3         6  
  3         1705  
16             __PACKAGE__->mk_accessors("width");
17             sub new {
18 229     229   436 my $self = shift;
19 229         773 return bless { width => 0, @_ }, $self;
20             }
21             sub is_penalty {
22 35     35   1546 return shift->isa("Text::KnuthPlass::Penalty");
23             }
24             sub is_glue {
25 0     0   0 return shift->isa("Text::KnuthPlass::Glue");
26             }
27              
28             package Text::KnuthPlass::Box;
29 3     3   6262 use base 'Text::KnuthPlass::Element';
  3         6  
  3         1375  
30             __PACKAGE__->mk_accessors("value");
31              
32             sub _txt {
33 0     0   0 return "[".$_[0]->value."/".$_[0]->width."]";
34             }
35              
36             package Text::KnuthPlass::Glue;
37 3     3   24 use base 'Text::KnuthPlass::Element';
  3         8  
  3         1105  
38             __PACKAGE__->mk_accessors("stretch", "shrink");
39              
40             sub new {
41 88     88   169 my $self = shift;
42 88         203 return $self->SUPER::new(stretch => 0, shrink => 0, @_);
43             }
44             sub _txt {
45 0     0   0 return sprintf "<%.2f+%.2f-%.2f>", $_[0]->width, $_[0]->stretch, $_[0]->shrink;
46             }
47              
48             package Text::KnuthPlass::Penalty;
49 3     3   35 use base 'Text::KnuthPlass::Element';
  3         5  
  3         1104  
50             __PACKAGE__->mk_accessors("penalty", "flagged", "shrink");
51             sub new {
52 29     29   279 my $self = shift;
53 29         98 return $self->SUPER::new(flagged => 0, shrink => 0, @_);
54             }
55             sub _txt {
56 0   0 0   0 return "(".$_[0]->penalty.($_[0]->flagged &&"!").")";
57             }
58              
59             package Text::KnuthPlass::Breakpoint;
60 3     3   23 use base 'Text::KnuthPlass::Element';
  3         6  
  3         851  
61             __PACKAGE__->mk_accessors(qw/position demerits ratio line fitnessClass totals previous/);
62              
63             package Text::KnuthPlass::DummyHyphenator;
64 3     3   40 use base 'Class::Accessor';
  3         7  
  3         374  
65             sub hyphenate {
66 58     58   541 return $_[1];
67             }
68              
69             package Text::KnuthPlass;
70 3     3   23 use base 'Class::Accessor';
  3         6  
  3         263  
71 3     3   19 use Carp qw/croak/;
  3         5  
  3         9210  
72              
73             my %defaults = (
74             infinity => 10000,
75             tolerance => 30,
76             hyphenpenalty => 50,
77             demerits => { line => 10, flagged => 100, fitness => 3000 },
78             space => { width => 3, stretch => 6, shrink => 9 },
79             linelengths => [78],
80             measure => sub { length $_[0] },
81             hyphenator =>
82             eval { require Text::Hyphen } ? Text::Hyphen->new() :
83             Text::KnuthPlass::DummyHyphenator->new()
84             );
85             __PACKAGE__->mk_accessors(keys %defaults);
86             sub new {
87 6     6 1 179146 my $self = shift;
88 6         86 return bless {%defaults, @_}, $self;
89             }
90              
91             =head1 NAME
92              
93             Text::KnuthPlass - Breaks paragraphs into lines using the TeX algorithm
94              
95             =head1 SYNOPSIS
96              
97             use Text::KnuthPlass;
98             my $typesetter = Text::KnuthPlass->new();
99             my @lines = $typesetter->typeset($paragraph);
100             ...
101              
102             To use with plain text:
103              
104             for (@lines) {
105             for (@{$_->{nodes}}) {
106             if ($_->isa("Text::KnuthPlass::Box")) { print $_->value }
107             elsif ($_->isa("Text::KnuthPlass::Glue")) { print " " }
108             }
109             if ($_->{nodes}[-1]->is_penalty) { print "-" }
110             print "\n";
111             }
112              
113             To use with PDF::Builder: (as well as PDF::API2)
114              
115             my $text = $page->text;
116             $text->font($font, 12);
117             $text->lead(13.5);
118              
119             my $t = Text::KnuthPlass->new(
120             measure => sub { $text->advancewidth(shift) },
121             linelengths => [235]
122             );
123             my @lines = $t->typeset($paragraph);
124              
125             my $y = 500;
126             for my $line (@lines) {
127             $x = 50;
128             for my $node (@{$line->{nodes}}) {
129             $text->translate($x,$y);
130             if ($node->isa("Text::KnuthPlass::Box")) {
131             $text->text($node->value);
132             $x += $node->width;
133             } elsif ($node->isa("Text::KnuthPlass::Glue")) {
134             $x += $node->width + $line->{ratio} *
135             ($line->{ratio} < 0 ? $node->shrink : $node->stretch);
136             }
137             }
138             if ($line->{nodes}[-1]->is_penalty) { $text->text("-") }
139             $y -= $text->lead();
140             }
141              
142             =head1 METHODS
143              
144             =head2 new
145              
146             The constructor takes a number of options. The most important ones are:
147              
148             =over 3
149              
150             =item measure
151              
152             A subroutine reference to determine the width of a piece of text. This
153             defaults to C, which is what you want if you're
154             typesetting plain monospaced text. You will need to change this to plug
155             into your font metrics if you're doing something graphical.
156              
157             =item linelengths
158              
159             This is an array of line lengths. For instance, C< [30,40,50] > will
160             typeset a triangle-shaped piece of text with three lines. What if the
161             text spills over to more than three lines? In that case, the final value
162             in the array is used for all further lines. So to typeset an ordinary
163             block-shaped column of text, you only need specify an array with one
164             value: the default is C< [78] >.
165              
166             =item tolerance
167              
168             How much leeway we have in leaving wider spaces than the algorithm
169             would prefer.
170              
171             =item hyphenator
172              
173             An object which hyphenates words. If you have C installed
174             (highly recommended) then a C object is instantiated by
175             default; if not, an object of the class
176             C is instantiated - this simply finds
177             no hyphenation points at all. So to turn hyphenation off, set
178              
179             hyphenator => Text::KnuthPlass::DummyHyphenator->new()
180              
181             To typeset non-English text, pass in an object which responds to the
182             C method, returning a list of hyphen positions. (See
183             C for the interface.)
184              
185             =back
186              
187             There are other options for fine-tuning the output. If you know your way
188             around TeX, dig into the source to find out what they are.
189              
190             =head2 typeset
191              
192             This is the main interface to the algorithm, made up of the constituent
193             parts below. It takes a paragraph of text and returns a list of lines if
194             suitable breakpoints could be found.
195              
196             The list has the following structure:
197              
198             (
199             { nodes => \@nodes, ratio => $ratio },
200             { nodes => \@nodes, ratio => $ratio },
201             ...
202             )
203              
204             The node list in each element will be a list of objects. Each object
205             will be either C, C
206             or C. See below for more on these.
207              
208             The C is the amount of stretch or shrink which should be applied to
209             each glue element in this line. The corrected width of each glue node
210             should be:
211              
212             $node->width + $line->{ratio} *
213             ($line->{ratio} < 0 ? $node->shrink : $node->stretch);
214              
215             Each box, glue or penalty node has a C attribute. Boxes have
216             Cs, which are the text which went into them; glue has C
217             and C to determine how much it should vary in width. That should
218             be all you need for basic typesetting; for more, see the source, and see
219             the original Knuth-Plass paper in "Digital Typography".
220              
221             This method is a thin wrapper around the three methods below.
222              
223             =cut
224              
225             sub typeset {
226 2     2 1 18 my ($t, $paragraph, @args) = @_;
227 2         15 my @nodes = $t->break_text_into_nodes($paragraph, @args);
228 2         12 my @breakpoints = $t->break(\@nodes);
229 2 50       6 return unless @breakpoints;
230 2         11 my @lines = $t->breakpoints_to_lines(\@breakpoints, \@nodes);
231             # Remove final penalty and glue
232 2 50       7 if (@lines) {
233 2         5 pop @{ $lines[-1]->{nodes} } ;
  2         6  
234 2         4 pop @{ $lines[-1]->{nodes} } ;
  2         4  
235             }
236 2         39 return @lines;
237             }
238              
239             =head2 break_text_into_nodes
240              
241             This turns a paragraph into a list of box/glue/penalty nodes. It's
242             fairly basic, and designed to be overloaded. It should also support
243             multiple justification styles (centering, ragged right, etc.) but this
244             will come in a future release; right now, it just does full
245             justification.
246              
247             If you are doing clever typography or using non-Western languages you
248             may find that you will want to break text into nodes yourself, and pass
249             the list of nodes to the methods below, instead of using this method.
250              
251             =cut
252              
253             sub _add_word {
254 88     88   156 my ($self, $word, $nodes_r) = @_;
255 88         171 my @elems = $self->hyphenator->hyphenate($word);
256 88         4515 for (0..$#elems) {
257 112         139 push @{$nodes_r}, Text::KnuthPlass::Box->new(
  112         256  
258             width => $self->measure->($elems[$_]),
259             value => $elems[$_]
260             );
261 112 100       290 if ($_ != $#elems) {
262 24         35 push @{$nodes_r}, Text::KnuthPlass::Penalty->new(
  24         54  
263             flagged => 1, penalty => $self->hyphenpenalty);
264             }
265             }
266 88         155 return;
267             }
268              
269             sub break_text_into_nodes {
270 5     5 1 662 my ($self, $text, $style) = @_;
271 5         9 my @nodes;
272 5         77 my @words = split /\s+/, $text;
273              
274 5         23 $self->{emwidth} = $self->measure->("M");
275 5         47 $self->{spacewidth} = $self->measure->(" ");
276 5         34 $self->{spacestretch} = $self->{spacewidth} * $self->space->{width} / $self->space->{stretch};
277 5         100 $self->{spaceshrink} = $self->{spacewidth} * $self->space->{width} / $self->space->{shrink};
278              
279 5   50     109 $style ||= "justify";
280 5         14 my $spacing_type = "_add_space_$style";
281 5         14 my $start = "_start_$style";
282 5         23 $self->$start(\@nodes);
283              
284 5         18 for (0..$#words) { my $word = $words[$_];
  88         134  
285 88         199 $self->_add_word($word, \@nodes);
286 88         200 $self->$spacing_type(\@nodes,$_ == $#words);
287             }
288 5         44 return @nodes;
289             }
290              
291             sub _start_justify {
292 5     5   10 return;
293             }
294             sub _add_space_justify {
295 88     88   165 my ($self, $nodes_r, $final) = @_;
296 88 100       139 if ($final) {
297 5         10 push @{$nodes_r},
  5         14  
298             $self->glueclass->new(
299             width => 0,
300             stretch => $self->infinity,
301             shrink => 0),
302             $self->penaltyclass->new(width => 0, penalty => -$self->infinity, flagged => 1);
303             } else {
304 83         146 push @{$nodes_r}, $self->glueclass->new(
305             width => $self->{spacewidth},
306             stretch => $self->{spacestretch},
307             shrink => $self->{spaceshrink}
308 83         102 );
309             }
310 88         168 return;
311             }
312              
313             sub _start_center {
314 0     0   0 my ($self, $nodes_r) = @_;
315 0         0 push @{$nodes_r},
316             Text::KnuthPlass::Box->new(value => ""),
317             Text::KnuthPlass::Glue->new(
318             width => 0,
319             stretch => 2*$self->{emwidth},
320 0         0 shrink => 0);
321 0         0 return;
322             }
323              
324             sub _add_space_center {
325 0     0   0 my ($self, $nodes_r, $final) = @_;
326 0 0       0 if ($final) {
327 0         0 push @{$nodes_r},
328 0         0 Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
329             Text::KnuthPlass::Penalty->new(width => 0, penalty => -$self->infinity, flagged => 0);
330             } else {
331 0         0 push @{$nodes_r},
332             Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
333             Text::KnuthPlass::Penalty->new(width => 0, penalty => 0, flagged => 0),
334             Text::KnuthPlass::Glue->new( width => $self->{spacewidth}, stretch => -4*$self->{emwidth}, shrink => 0),
335             Text::KnuthPlass::Box->new(value => ""),
336             Text::KnuthPlass::Penalty->new(width => 0, penalty => $self->infinity, flagged => 0),
337 0         0 Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
338             }
339 0         0 return;
340             }
341              
342             =head2 break
343              
344             This implements the main body of the algorithm; it turns a list of nodes
345             (produced from the above method) into a list of breakpoint objects.
346              
347             =cut
348              
349             sub _init_nodelist { # Overridden by XS
350             shift->{activeNodes} = [
351             Text::KnuthPlass::Breakpoint->new(position => 0,
352             demerits => 0,
353             ratio => 0,
354             line => 0,
355             fitnessClass => 0,
356             totals => { width => 0, stretch => 0, shrink => 0}
357             )
358             ];
359             return;
360             }
361              
362             sub break {
363 3     3 1 1878 my ($self, $nodes) = @_;
364 3         14 $self->{sum} = {width => 0, stretch => 0, shrink => 0 };
365 3         26 $self->_init_nodelist();
366 3 50 33     30 if (!$self->{linelengths} || ref $self->{linelengths} ne "ARRAY") {
367 0         0 croak "No linelengths set";
368             }
369              
370 3         13 for (0..$#$nodes) {
371 201         1719 my $node = $nodes->[$_];
372 201 100 33     573 if ($node->isa("Text::KnuthPlass::Box")) {
    100          
    50          
373 99         195 $self->{sum}{width} += $node->width;
374             } elsif ($node->isa("Text::KnuthPlass::Glue")) {
375 81 50 33     325 if ($_ > 0 and $nodes->[$_-1]->isa("Text::KnuthPlass::Box")) {
376 81         3305 $self->_mainloop($node, $_, $nodes);
377             }
378 81         205 $self->{sum}{width} += $node->width;
379 81         843 $self->{sum}{stretch} += $node->stretch;
380 81         743 $self->{sum}{shrink} += $node->shrink;
381             } elsif ($node->is_penalty and $node->penalty != $self->infinity) {
382 21         1096 $self->_mainloop($node, $_, $nodes);
383             }
384             }
385              
386 3         25 my @retval = reverse $self->_active_to_breaks;
387 3         14 $self->_cleanup;
388 3         10 return @retval;
389             }
390              
391             sub _cleanup { return; }
392              
393             sub _active_to_breaks { # Overridden by XS
394             my $self = shift;
395             return unless @{$self->{activeNodes}};
396             my @breaks;
397             my $tmp = Text::KnuthPlass::Breakpoint->new(demerits => ~0);
398             for (@{$self->{activeNodes}}) { $tmp = $_ if $_->demerits < $tmp->demerits }
399             while ($tmp) {
400             push @breaks, { position => $tmp->position,
401             ratio => $tmp->ratio
402             };
403             $tmp = $tmp->previous
404             }
405             return @breaks;
406             }
407              
408             sub _mainloop {
409             my ($self, $node, $index, $nodes) = @_;
410             my $next; my $ratio = 0; my $demerits = 0; my @candidates;
411             my $badness; my $currentLine = 0; my $tmpSum; my $currentClass = 0;
412             my $active = $self->{activeNodes}[0];
413             my $ptr = 0;
414             while ($active) {
415             @candidates = ( {demerits => ~0}, {demerits => ~0},{demerits => ~0},{demerits => ~0} );
416             warn "Outer\n" if DEBUG;
417             while ($active) {
418             my $next = $self->{activeNodes}[++$ptr];
419             warn "Inner loop\n" if DEBUG;
420             $currentLine = $active->line+1;
421             $ratio = $self->_computeCost($active->position, $index, $active, $currentLine, $nodes);
422             warn "Got a ratio of $ratio, node is ".$node->_txt."\n" if DEBUG;
423             if ($ratio < -1 or
424             ($node->is_penalty and $node->penalty == -$self->infinity)) {
425             warn "Dropping a node\n" if DEBUG;
426             $self->{activeNodes} = [ grep {$_ != $active} @{$self->{activeNodes}} ];
427             $ptr--;
428             }
429             if (-1 <= $ratio and $ratio <= $self->tolerance) {
430             $badness = 100 * $ratio**3;
431             warn "Badness is $badness\n" if DEBUG;
432             if ($node->is_penalty and $node->penalty > 0) {
433             $demerits = ($self->demerits->{line} + $badness +
434             $node->penalty)**2;
435             } elsif ($node->is_penalty and $node->penalty != -$self->infinity) {
436             $demerits = ($self->demerits->{line} + $badness -
437             $node->penalty)**2;
438             } else {
439             $demerits = ($self->demerits->{line} + $badness)**2;
440             }
441              
442             if ($node->is_penalty and $nodes->[$active->position]->is_penalty) {
443             $demerits += $self->demerits->{flagged} *
444             $node->flagged *
445             $nodes->[$active->position]->flagged;
446             }
447              
448             if ($ratio < -0.5) { $currentClass = 0 }
449             elsif ($ratio <= 0.5) { $currentClass = 1 }
450             elsif ($ratio <= 1 ) { $currentClass = 2 }
451             else { $currentClass = 3 }
452              
453             $demerits += $self->demerits->{fitness}
454             if abs($currentClass - $active->fitnessClass) > 1;
455              
456             $demerits += $active->demerits;
457             if ($demerits < $candidates[$currentClass]->{demerits}) {
458             warn "Setting c $currentClass\n" if DEBUG;
459             $candidates[$currentClass] = { active => $active,
460             demerits => $demerits,
461             ratio => $ratio
462             };
463             }
464             }
465             $active = $next;
466             #warn "Active is now $active" if DEBUG;
467             last if !$active ||
468             $active->line >= $currentLine;
469             }
470             warn "Post inner loop\n" if DEBUG;
471             $tmpSum = $self->_computeSum($index, $nodes);
472             for (0..3) { my $c = $candidates[$_];
473             if ($c->{demerits} < ~0) {
474             my $newnode = Text::KnuthPlass::Breakpoint->new(
475             position => $index,
476             demerits => $c->{demerits},
477             ratio => $c->{ratio},
478             line => $c->{active}->line + 1,
479             fitnessClass => $_,
480             totals => $tmpSum,
481             previous => $c->{active}
482             );
483             if ($active) {
484             warn "Before\n" if DEBUG;
485             my @newlist;
486             for (@{$self->{activeNodes}}) {
487             if ($_ == $active) { push @newlist, $newnode }
488             push @newlist, $_;
489             }
490             $ptr++;
491             $self->{activeNodes} = [ @newlist ];
492             # grep {;
493             # ($_ == $active) ? ($newnode, $active) : ($_)
494             #} @{$self->{activeNodes}}
495             # ];
496             }
497             else {
498             warn "After\n" if DEBUG;
499             push @{$self->{activeNodes}}, $newnode
500             }
501             #warn @{$self->{activeNodes}} if DEBUG;
502             }
503             }
504             }
505             return;
506             }
507              
508             sub _computeCost {
509 0     0   0 my ($self, $start, $end, $active, $currentLine, $nodes) = @_;
510 0         0 warn "Computing cost from $start to $end\n" if DEBUG;
511 0         0 warn sprintf "Sum width: %f\n", $self->{sum}{width} if DEBUG;
512 0         0 warn sprintf "Total width: %f\n", $self->{totals}{width} if DEBUG;
513 0         0 my $width = $self->{sum}{width} - $active->totals->{width};
514 0         0 my $stretch = 0; my $shrink = 0;
  0         0  
515 0         0 my $linelength = $currentLine < @{$self->linelengths} ?
516             $self->{linelengths}[$currentLine-1] :
517 0 0       0 $self->{linelengths}[-1];
518              
519 0 0 0     0 warn "Adding penalty width" if($nodes->[$end]->is_penalty) and DEBUG;
520 0 0       0 $width += $nodes->[$end]->width if $nodes->[$end]->is_penalty;
521 0         0 warn sprintf "Width %f, linelength %f\n", $width, $linelength if DEBUG;
522 0 0       0 if ($width < $linelength) {
    0          
523 0         0 $stretch = $self->{sum}{stretch} - $active->totals->{stretch};
524 0         0 warn sprintf "Stretch %f\n", $stretch if DEBUG;
525 0 0       0 if ($stretch > 0) {
526 0         0 return ($linelength - $width) / $stretch;
527 0         0 } else { return $self->infinity(); }
528             } elsif ($width > $linelength) {
529 0         0 $shrink = $self->{sum}{shrink} - $active->totals->{shrink};
530 0         0 warn sprintf "Shrink %f\n", $shrink if DEBUG;
531 0 0       0 if ($shrink > 0) {
532 0         0 return ($linelength - $width) / $shrink;
533 0         0 } else { return $self->infinity}
534 0         0 } else { return 0; }
535             }
536              
537             sub _computeSum {
538 0     0   0 my ($self, $index, $nodes) = @_;
539             my $result = { width => $self->{sum}{width},
540 0         0 stretch => $self->{sum}{stretch}, shrink => $self->{sum}{shrink} };
541 0         0 for ($index..$#$nodes) {
542 0 0 0     0 if ($nodes->[$_]->isa("Text::KnuthPlass::Glue")) {
    0 0        
      0        
543 0         0 $result->{width} += $nodes->[$_]->width;
544 0         0 $result->{stretch} += $nodes->[$_]->stretch;
545 0         0 $result->{shrink} += $nodes->[$_]->shrink;
546             } elsif ($nodes->[$_]->isa("Text::KnuthPlass::Box") or
547             ($nodes->[$_]->is_penalty and $nodes->[$_]->penalty ==
548 0         0 -$self->infinity and $_ > $index)) { last }
549             }
550 0         0 return $result;
551             }
552              
553             =head2 breakpoints_to_lines
554              
555             And this takes the breakpoints and the nodes, and assembles them into
556             lines.
557              
558             =cut
559              
560             sub breakpoints_to_lines {
561 2     2 1 7 my ($self, $breakpoints, $nodes) = @_;
562 2         4 my @lines;
563 2         13 my $linestart = 0;
564 2         10 for my $x (1 .. $#$breakpoints) { $_ = $breakpoints->[$x];
  8         14  
565 8         12 my $position = $_->{position};
566 8         14 my $r = $_->{ratio};
567 8         16 for ($linestart..$#$nodes) {
568 14 100 66     103 if ($nodes->[$_]->isa("Text::KnuthPlass::Box") or
      66        
569             ($nodes->[$_]->is_penalty and $nodes->[$_]->penalty ==-$self->infinity)) {
570 8         13 $linestart = $_;
571 8         12 last;
572             }
573             }
574             push @lines, { ratio => $r, position => $_->{position},
575 8         17 nodes => [ @{$nodes}[$linestart..$position] ]};
  8         42  
576 8         20 $linestart = $_->{position};
577             }
578             #if ($linestart < $#$nodes) {
579             # push @lines, { ratio => 1, position => $#$nodes,
580             # nodes => [ @{$nodes}[$linestart+1..$#$nodes] ]};
581             #}
582 2         7 return @lines;
583             }
584              
585             =head2 glueclass
586              
587             =head2 penaltyclass
588              
589             For subclassers.
590              
591             =cut
592              
593             sub glueclass {
594 88     88 1 211 return "Text::KnuthPlass::Glue";
595             }
596             sub penaltyclass {
597 5     5 1 17 return "Text::KnuthPlass::Penalty";
598             }
599              
600             =head1 AUTHOR
601              
602             originally written by Simon Cozens, C<< >>
603              
604             since 2020, maintained by Phil Perry
605              
606             =head1 ACKNOWLEDGEMENTS
607              
608             This module is a Perl translation of Bram Stein's "Typeset" Javascript
609             Knuth-Plass implementation. Any bugs, however, are probably my fault.
610              
611             =head1 BUGS
612              
613             Please report any bugs or feature requests to the _issues_ section of
614             C.
615              
616             Do NOT under ANY circumstances open a PR (Pull Request) to report a bug. It is
617             a waste of both your and our time and effort. Open a regular ticket (issue),
618             and attach a Perl (.pl) program illustrating the problem, if possible. If you
619             believe that you have a program patch, and offer to share it as a PR, we may
620             give the go-ahead. Unsolicited PRs may be closed without further action.
621              
622             =head1 COPYRIGHT & LICENSE
623              
624             Copyright (c) 2011 Simon Cozens.
625              
626             Copyright (c) 2020 Phil M Perry.
627              
628             This program is released under the following license: Perl, GPL
629              
630             =cut
631              
632             1; # End of Text::KnuthPlass