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   148618 use constant DEBUG => 0;
  3         17  
  3         335  
4 3     3   21 use warnings;
  3         5  
  3         84  
5 3     3   16 use strict;
  3         4  
  3         215  
6              
7             our $VERSION = '1.06'; # VERSION
8             my $LAST_UPDATE = '1.06'; # 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   2396 use Data::Dumper;
  3         21802  
  3         251  
13              
14             package Text::KnuthPlass::Element;
15 3     3   28 use base 'Class::Accessor';
  3         7  
  3         1862  
16             __PACKAGE__->mk_accessors("width");
17             sub new {
18 229     229   424 my $self = shift;
19 229         765 return bless { width => 0, @_ }, $self;
20             }
21             sub is_penalty {
22 35     35   1541 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   6378 use base 'Text::KnuthPlass::Element';
  3         10  
  3         1452  
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   25 use base 'Text::KnuthPlass::Element';
  3         8  
  3         1115  
38             __PACKAGE__->mk_accessors("stretch", "shrink");
39              
40             sub new {
41 88     88   178 my $self = shift;
42 88         174 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   24 use base 'Text::KnuthPlass::Element';
  3         4  
  3         1108  
50             __PACKAGE__->mk_accessors("penalty", "flagged", "shrink");
51             sub new {
52 29     29   273 my $self = shift;
53 29         64 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   24 use base 'Text::KnuthPlass::Element';
  3         5  
  3         913  
61             __PACKAGE__->mk_accessors(qw/position demerits ratio line fitnessClass totals previous/);
62              
63             package Text::KnuthPlass::DummyHyphenator;
64 3     3   43 use base 'Class::Accessor';
  3         8  
  3         376  
65             sub hyphenate {
66 58     58   554 return $_[1];
67             }
68              
69             package Text::KnuthPlass;
70 3     3   23 use base 'Class::Accessor';
  3         6  
  3         245  
71 3     3   21 use Carp qw/croak/;
  3         5  
  3         9310  
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 177011 my $self = shift;
88 6         87 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             Why I rather than something like I? Per
222             L, this code is ported from the Javascript product
223             B.
224              
225             This method is a thin wrapper around the three methods below.
226              
227             =cut
228              
229             sub typeset {
230 2     2 1 18 my ($t, $paragraph, @args) = @_;
231 2         14 my @nodes = $t->break_text_into_nodes($paragraph, @args);
232 2         13 my @breakpoints = $t->break(\@nodes);
233 2 50       7 return unless @breakpoints;
234 2         12 my @lines = $t->breakpoints_to_lines(\@breakpoints, \@nodes);
235             # Remove final penalty and glue
236 2 50       6 if (@lines) {
237 2         4 pop @{ $lines[-1]->{nodes} } ;
  2         6  
238 2         4 pop @{ $lines[-1]->{nodes} } ;
  2         4  
239             }
240 2         38 return @lines;
241             }
242              
243             =head2 break_text_into_nodes
244              
245             This turns a paragraph into a list of box/glue/penalty nodes. It's
246             fairly basic, and designed to be overloaded. It should also support
247             multiple justification styles (centering, ragged right, etc.) but this
248             will come in a future release; right now, it just does full
249             justification.
250              
251             If you are doing clever typography or using non-Western languages you
252             may find that you will want to break text into nodes yourself, and pass
253             the list of nodes to the methods below, instead of using this method.
254              
255             =cut
256              
257             sub _add_word {
258 88     88   149 my ($self, $word, $nodes_r) = @_;
259 88         167 my @elems = $self->hyphenator->hyphenate($word);
260 88         4479 for (0..$#elems) {
261 112         137 push @{$nodes_r}, Text::KnuthPlass::Box->new(
  112         252  
262             width => $self->measure->($elems[$_]),
263             value => $elems[$_]
264             );
265 112 100       262 if ($_ != $#elems) {
266 24         34 push @{$nodes_r}, Text::KnuthPlass::Penalty->new(
  24         78  
267             flagged => 1, penalty => $self->hyphenpenalty);
268             }
269             }
270 88         171 return;
271             }
272              
273             sub break_text_into_nodes {
274 5     5 1 669 my ($self, $text, $style) = @_;
275 5         11 my @nodes;
276 5         76 my @words = split /\s+/, $text;
277              
278 5         25 $self->{emwidth} = $self->measure->("M");
279 5         84 $self->{spacewidth} = $self->measure->(" ");
280 5         38 $self->{spacestretch} = $self->{spacewidth} * $self->space->{width} / $self->space->{stretch};
281 5         96 $self->{spaceshrink} = $self->{spacewidth} * $self->space->{width} / $self->space->{shrink};
282              
283 5   50     109 $style ||= "justify";
284 5         15 my $spacing_type = "_add_space_$style";
285 5         13 my $start = "_start_$style";
286 5         21 $self->$start(\@nodes);
287              
288 5         19 for (0..$#words) { my $word = $words[$_];
  88         137  
289 88         194 $self->_add_word($word, \@nodes);
290 88         197 $self->$spacing_type(\@nodes,$_ == $#words);
291             }
292 5         41 return @nodes;
293             }
294              
295             sub _start_justify {
296 5     5   8 return;
297             }
298             sub _add_space_justify {
299 88     88   160 my ($self, $nodes_r, $final) = @_;
300 88 100       146 if ($final) {
301 5         7 push @{$nodes_r},
  5         15  
302             $self->glueclass->new(
303             width => 0,
304             stretch => $self->infinity,
305             shrink => 0),
306             $self->penaltyclass->new(width => 0, penalty => -$self->infinity, flagged => 1);
307             } else {
308 83         146 push @{$nodes_r}, $self->glueclass->new(
309             width => $self->{spacewidth},
310             stretch => $self->{spacestretch},
311             shrink => $self->{spaceshrink}
312 83         103 );
313             }
314 88         174 return;
315             }
316              
317             sub _start_center {
318 0     0   0 my ($self, $nodes_r) = @_;
319 0         0 push @{$nodes_r},
320             Text::KnuthPlass::Box->new(value => ""),
321             Text::KnuthPlass::Glue->new(
322             width => 0,
323             stretch => 2*$self->{emwidth},
324 0         0 shrink => 0);
325 0         0 return;
326             }
327              
328             sub _add_space_center {
329 0     0   0 my ($self, $nodes_r, $final) = @_;
330 0 0       0 if ($final) {
331 0         0 push @{$nodes_r},
332 0         0 Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
333             Text::KnuthPlass::Penalty->new(width => 0, penalty => -$self->infinity, flagged => 0);
334             } else {
335 0         0 push @{$nodes_r},
336             Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
337             Text::KnuthPlass::Penalty->new(width => 0, penalty => 0, flagged => 0),
338             Text::KnuthPlass::Glue->new( width => $self->{spacewidth}, stretch => -4*$self->{emwidth}, shrink => 0),
339             Text::KnuthPlass::Box->new(value => ""),
340             Text::KnuthPlass::Penalty->new(width => 0, penalty => $self->infinity, flagged => 0),
341 0         0 Text::KnuthPlass::Glue->new( width => 0, stretch => 2*$self->{emwidth}, shrink => 0),
342             }
343 0         0 return;
344             }
345              
346             =head2 break
347              
348             This implements the main body of the algorithm; it turns a list of nodes
349             (produced from the above method) into a list of breakpoint objects.
350              
351             =cut
352              
353             sub _init_nodelist { # Overridden by XS
354             shift->{activeNodes} = [
355             Text::KnuthPlass::Breakpoint->new(position => 0,
356             demerits => 0,
357             ratio => 0,
358             line => 0,
359             fitnessClass => 0,
360             totals => { width => 0, stretch => 0, shrink => 0}
361             )
362             ];
363             return;
364             }
365              
366             sub break {
367 3     3 1 1955 my ($self, $nodes) = @_;
368 3         15 $self->{sum} = {width => 0, stretch => 0, shrink => 0 };
369 3         29 $self->_init_nodelist();
370 3 50 33     27 if (!$self->{linelengths} || ref $self->{linelengths} ne "ARRAY") {
371 0         0 croak "No linelengths set";
372             }
373              
374 3         14 for (0..$#$nodes) {
375 201         1642 my $node = $nodes->[$_];
376 201 100 33     620 if ($node->isa("Text::KnuthPlass::Box")) {
    100          
    50          
377 99         222 $self->{sum}{width} += $node->width;
378             } elsif ($node->isa("Text::KnuthPlass::Glue")) {
379 81 50 33     325 if ($_ > 0 and $nodes->[$_-1]->isa("Text::KnuthPlass::Box")) {
380 81         3047 $self->_mainloop($node, $_, $nodes);
381             }
382 81         205 $self->{sum}{width} += $node->width;
383 81         798 $self->{sum}{stretch} += $node->stretch;
384 81         738 $self->{sum}{shrink} += $node->shrink;
385             } elsif ($node->is_penalty and $node->penalty != $self->infinity) {
386 21         1079 $self->_mainloop($node, $_, $nodes);
387             }
388             }
389              
390 3         28 my @retval = reverse $self->_active_to_breaks;
391 3         10 $self->_cleanup;
392 3         12 return @retval;
393             }
394              
395             sub _cleanup { return; }
396              
397             sub _active_to_breaks { # Overridden by XS
398             my $self = shift;
399             return unless @{$self->{activeNodes}};
400             my @breaks;
401             my $tmp = Text::KnuthPlass::Breakpoint->new(demerits => ~0);
402             for (@{$self->{activeNodes}}) { $tmp = $_ if $_->demerits < $tmp->demerits }
403             while ($tmp) {
404             push @breaks, { position => $tmp->position,
405             ratio => $tmp->ratio
406             };
407             $tmp = $tmp->previous
408             }
409             return @breaks;
410             }
411              
412             sub _mainloop {
413             my ($self, $node, $index, $nodes) = @_;
414             my $next; my $ratio = 0; my $demerits = 0; my @candidates;
415             my $badness; my $currentLine = 0; my $tmpSum; my $currentClass = 0;
416             my $active = $self->{activeNodes}[0];
417             my $ptr = 0;
418             while ($active) {
419             @candidates = ( {demerits => ~0}, {demerits => ~0},{demerits => ~0},{demerits => ~0} );
420             warn "Outer\n" if DEBUG;
421             while ($active) {
422             my $next = $self->{activeNodes}[++$ptr];
423             warn "Inner loop\n" if DEBUG;
424             $currentLine = $active->line+1;
425             $ratio = $self->_computeCost($active->position, $index, $active, $currentLine, $nodes);
426             warn "Got a ratio of $ratio, node is ".$node->_txt."\n" if DEBUG;
427             if ($ratio < -1 or
428             ($node->is_penalty and $node->penalty == -$self->infinity)) {
429             warn "Dropping a node\n" if DEBUG;
430             $self->{activeNodes} = [ grep {$_ != $active} @{$self->{activeNodes}} ];
431             $ptr--;
432             }
433             if (-1 <= $ratio and $ratio <= $self->tolerance) {
434             $badness = 100 * $ratio**3;
435             warn "Badness is $badness\n" if DEBUG;
436             if ($node->is_penalty and $node->penalty > 0) {
437             $demerits = ($self->demerits->{line} + $badness +
438             $node->penalty)**2;
439             } elsif ($node->is_penalty and $node->penalty != -$self->infinity) {
440             $demerits = ($self->demerits->{line} + $badness -
441             $node->penalty)**2;
442             } else {
443             $demerits = ($self->demerits->{line} + $badness)**2;
444             }
445              
446             if ($node->is_penalty and $nodes->[$active->position]->is_penalty) {
447             $demerits += $self->demerits->{flagged} *
448             $node->flagged *
449             $nodes->[$active->position]->flagged;
450             }
451              
452             if ($ratio < -0.5) { $currentClass = 0 }
453             elsif ($ratio <= 0.5) { $currentClass = 1 }
454             elsif ($ratio <= 1 ) { $currentClass = 2 }
455             else { $currentClass = 3 }
456              
457             $demerits += $self->demerits->{fitness}
458             if abs($currentClass - $active->fitnessClass) > 1;
459              
460             $demerits += $active->demerits;
461             if ($demerits < $candidates[$currentClass]->{demerits}) {
462             warn "Setting c $currentClass\n" if DEBUG;
463             $candidates[$currentClass] = { active => $active,
464             demerits => $demerits,
465             ratio => $ratio
466             };
467             }
468             }
469             $active = $next;
470             #warn "Active is now $active" if DEBUG;
471             last if !$active ||
472             $active->line >= $currentLine;
473             }
474             warn "Post inner loop\n" if DEBUG;
475             $tmpSum = $self->_computeSum($index, $nodes);
476             for (0..3) { my $c = $candidates[$_];
477             if ($c->{demerits} < ~0) {
478             my $newnode = Text::KnuthPlass::Breakpoint->new(
479             position => $index,
480             demerits => $c->{demerits},
481             ratio => $c->{ratio},
482             line => $c->{active}->line + 1,
483             fitnessClass => $_,
484             totals => $tmpSum,
485             previous => $c->{active}
486             );
487             if ($active) {
488             warn "Before\n" if DEBUG;
489             my @newlist;
490             for (@{$self->{activeNodes}}) {
491             if ($_ == $active) { push @newlist, $newnode }
492             push @newlist, $_;
493             }
494             $ptr++;
495             $self->{activeNodes} = [ @newlist ];
496             # grep {;
497             # ($_ == $active) ? ($newnode, $active) : ($_)
498             #} @{$self->{activeNodes}}
499             # ];
500             }
501             else {
502             warn "After\n" if DEBUG;
503             push @{$self->{activeNodes}}, $newnode
504             }
505             #warn @{$self->{activeNodes}} if DEBUG;
506             }
507             }
508             }
509             return;
510             }
511              
512             sub _computeCost {
513 0     0   0 my ($self, $start, $end, $active, $currentLine, $nodes) = @_;
514 0         0 warn "Computing cost from $start to $end\n" if DEBUG;
515 0         0 warn sprintf "Sum width: %f\n", $self->{sum}{width} if DEBUG;
516 0         0 warn sprintf "Total width: %f\n", $self->{totals}{width} if DEBUG;
517 0         0 my $width = $self->{sum}{width} - $active->totals->{width};
518 0         0 my $stretch = 0; my $shrink = 0;
  0         0  
519 0         0 my $linelength = $currentLine <= @{$self->linelengths} ?
520             $self->{linelengths}[$currentLine-1] :
521 0 0       0 $self->{linelengths}[-1];
522              
523 0 0 0     0 warn "Adding penalty width" if($nodes->[$end]->is_penalty) and DEBUG;
524 0 0       0 $width += $nodes->[$end]->width if $nodes->[$end]->is_penalty;
525 0         0 warn sprintf "Width %f, linelength %f\n", $width, $linelength if DEBUG;
526 0 0       0 if ($width < $linelength) {
    0          
527 0         0 $stretch = $self->{sum}{stretch} - $active->totals->{stretch};
528 0         0 warn sprintf "Stretch %f\n", $stretch if DEBUG;
529 0 0       0 if ($stretch > 0) {
530 0         0 return ($linelength - $width) / $stretch;
531 0         0 } else { return $self->infinity(); }
532             } elsif ($width > $linelength) {
533 0         0 $shrink = $self->{sum}{shrink} - $active->totals->{shrink};
534 0         0 warn sprintf "Shrink %f\n", $shrink if DEBUG;
535 0 0       0 if ($shrink > 0) {
536 0         0 return ($linelength - $width) / $shrink;
537 0         0 } else { return $self->infinity}
538 0         0 } else { return 0; }
539             }
540              
541             sub _computeSum {
542 0     0   0 my ($self, $index, $nodes) = @_;
543             my $result = { width => $self->{sum}{width},
544 0         0 stretch => $self->{sum}{stretch}, shrink => $self->{sum}{shrink} };
545 0         0 for ($index..$#$nodes) {
546 0 0 0     0 if ($nodes->[$_]->isa("Text::KnuthPlass::Glue")) {
    0 0        
      0        
547 0         0 $result->{width} += $nodes->[$_]->width;
548 0         0 $result->{stretch} += $nodes->[$_]->stretch;
549 0         0 $result->{shrink} += $nodes->[$_]->shrink;
550             } elsif ($nodes->[$_]->isa("Text::KnuthPlass::Box") or
551             ($nodes->[$_]->is_penalty and $nodes->[$_]->penalty ==
552 0         0 -$self->infinity and $_ > $index)) { last }
553             }
554 0         0 return $result;
555             }
556              
557             =head2 breakpoints_to_lines
558              
559             And this takes the breakpoints and the nodes, and assembles them into
560             lines.
561              
562             =cut
563              
564             sub breakpoints_to_lines {
565 2     2 1 6 my ($self, $breakpoints, $nodes) = @_;
566 2         3 my @lines;
567 2         15 my $linestart = 0;
568 2         11 for my $x (1 .. $#$breakpoints) { $_ = $breakpoints->[$x];
  8         13  
569 8         15 my $position = $_->{position};
570 8         12 my $r = $_->{ratio};
571 8         17 for ($linestart..$#$nodes) {
572 14 100 66     95 if ($nodes->[$_]->isa("Text::KnuthPlass::Box") or
      66        
573             ($nodes->[$_]->is_penalty and $nodes->[$_]->penalty ==-$self->infinity)) {
574 8         12 $linestart = $_;
575 8         14 last;
576             }
577             }
578             push @lines, { ratio => $r, position => $_->{position},
579 8         19 nodes => [ @{$nodes}[$linestart..$position] ]};
  8         40  
580 8         21 $linestart = $_->{position};
581             }
582             #if ($linestart < $#$nodes) {
583             # push @lines, { ratio => 1, position => $#$nodes,
584             # nodes => [ @{$nodes}[$linestart+1..$#$nodes] ]};
585             #}
586 2         7 return @lines;
587             }
588              
589             =head2 glueclass
590              
591             =head2 penaltyclass
592              
593             For subclassers.
594              
595             =cut
596              
597             sub glueclass {
598 88     88 1 220 return "Text::KnuthPlass::Glue";
599             }
600             sub penaltyclass {
601 5     5 1 14 return "Text::KnuthPlass::Penalty";
602             }
603              
604             =head1 AUTHOR
605              
606             originally written by Simon Cozens, C<< >>
607              
608             since 2020, maintained by Phil Perry
609              
610             =head1 ACKNOWLEDGEMENTS
611              
612             This module is a Perl translation of Bram Stein's "Typeset" Javascript
613             Knuth-Plass implementation. Any bugs, however, are probably my fault.
614              
615             =head1 BUGS
616              
617             Please report any bugs or feature requests to the I section of
618             C.
619              
620             Do NOT under ANY circumstances open a PR (Pull Request) to report a bug. It is
621             a waste of both your and our time and effort. Open a regular ticket (issue),
622             and attach a Perl (.pl) program illustrating the problem, if possible. If you
623             believe that you have a program patch, and offer to share it as a PR, we may
624             give the go-ahead. Unsolicited PRs may be closed without further action.
625              
626             =head1 COPYRIGHT & LICENSE
627              
628             Copyright (c) 2011 Simon Cozens.
629              
630             Copyright (c) 2020-2021 Phil M Perry.
631              
632             This program is released under the following license: Perl, GPL
633              
634             =cut
635              
636             1; # End of Text::KnuthPlass