File Coverage

blib/lib/JSON/JSONFold.pm
Criterion Covered Total %
statement 528 699 75.5
branch 131 216 60.6
condition 82 130 63.0
subroutine 101 130 77.6
pod 12 14 85.7
total 854 1189 71.8


line stmt bran cond sub pod time code
1             package JSON::JSONFold ;
2              
3 5     5   599011 use strict;
  5         7  
  5         159  
4 5     5   17 use warnings;
  5         10  
  5         264  
5 5     5   69 use 5.014 ;
  5         19  
6 5     5   16 use JSON::PP ();
  5         8  
  5         122  
7              
8 5     5   22 use Exporter 'import';
  5         8  
  5         5657  
9              
10             our $VERSION = '0.2.0';
11             our @EXPORT = qw(
12             format_json write_json fold_text
13             encode_json to_json) ;
14              
15             our @EXPORT_OK = qw(
16             jsonfold_config
17             create_writer
18             ) ;
19             # Object Orient Interface
20              
21             sub new {
22 19     19 1 50 my ($class, %overrides) = @_ ;
23              
24             # Required parameters
25 19         31 my $width = delete $overrides{width} ;
26 19         29 my $config = delete $overrides{config} ;
27              
28 19   100     67 my $gold = delete $overrides{gold} // 1 ;
29 19         28 my $indent = delete $overrides{indent} ;
30 19   33     58 my $json = delete $overrides{json} // _json_coder($gold, $indent, %overrides) ;
31 19         27 my $do_close = delete $overrides{do_close} ;
32 19   50     42 return bless {
33             json => $json,
34             width => $width,
35             config => _config($config, $width),
36             do_close => $do_close,
37             }, ref($class) || $class || __PACKAGE__ ;
38             }
39              
40             sub format {
41 18     18 1 26 my($self, $data) = @_ ;
42              
43 18         36 my $config = $self->{config} ;
44              
45 18         22 my $output = '' ;
46 18 50       188 open my $out, '>', \$output or die "open output: $!" ;
47              
48 18         34 my $stream = _stream($out, $config, 0) ;
49 18         23 my $json = $self->{json} ;
50 18         40 my $text = $json->encode($data) ;
51              
52 18         3100 $stream->write($text);
53              
54 18 50       64 close $out or die "close output: $!" ;
55 18 50       63 $output .= "\n" unless $output =~ /\n\z/;
56 18         119 return $output ;
57             }
58              
59             sub fold {
60 0     0 1 0 my ($self, $text) = @_ ;
61              
62 0         0 my $config = $self->{config} ;
63              
64 0         0 my $output = '' ;
65 0 0       0 open my $out, '>', \$output or die "open output: $!" ;
66              
67 0         0 my $stream = _stream($out, $config, 0) ;
68              
69 0         0 $stream->write($text);
70              
71 0 0       0 close $out or die "close output: $!" ;
72 0 0       0 $output .= "\n" unless $output =~ /\n\z/;
73 0         0 return $output ;
74             }
75              
76             sub write {
77 1     1 1 3 my($self, $data, $fh) = @_ ;
78              
79 1         7 my $config = $self->{config} ;
80              
81 1         2 my $do_close = $self->{do_close} ;
82 1         5 my $stream = _stream($fh, $config, $do_close) ;
83 1         3 my $json = $self->{json} ;
84              
85 1         10 my $text = $json->encode($data) ;
86 1         290 $stream->write($text);
87 1         6 $stream->finish;
88 1         4 $stream->flush;
89 1         20 my $info = $stream->stats ;
90 1         5 $stream->close() ;
91              
92 1         11 return $info ;
93             }
94              
95             # Functional Interface
96              
97             # Not exportable. Allow using DEV::JSONFold::config(...)
98             sub config {
99 0     0 0 0 my($base_config, $width, %overrides) = @_ ;
100 0   0     0 $width //= $overrides{width} ;
101 0         0 return _config($base_config, $width, %overrides) ;
102             }
103              
104             sub jsonfold_config {
105 1     1 1 6 my($base_config, $width, %overrides) = @_ ;
106 1   33     9 $width //= $overrides{width} ;
107 1         5 return _config($base_config, $width, %overrides) ;
108             }
109              
110             sub format_json {
111 0     0 1 0 my($data, $width, $config, %overrides) = @_ ;
112              
113 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $config, %overrides) ;
114 0         0 my $output = $fmt->format($data) ;
115 0         0 return $output ;
116             }
117              
118             sub write_json {
119 1     1 1 7 my($data, $fh, $width, $config, %overrides) = @_ ;
120              
121 1         11 my $fmt = __PACKAGE__->new(width => $width, config => $config, %overrides) ;
122 1         6 my $info = $fmt->write($data, $fh) ;
123 1         16 return $info ;
124             }
125              
126             sub fold_text {
127 0     0 1 0 my($text, $width, $config) = @_ ;
128 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $config) ;
129              
130 0         0 return $fmt->fold($text) ;
131             }
132              
133             sub create_writer {
134 0     0 1 0 my($fh, $width, $config, %overrides) = @_ ;
135 0         0 my $do_close = delete $overrides{close_fp} ;
136 0         0 return _stream($fh, _config($config, $width, %overrides), $do_close) ;
137             }
138              
139             # Helper for Function/OO interface methods
140              
141             sub _config {
142 20     20   36 my ($preset, $width, %overrides) = @_ ;
143 20 50       45 $overrides{width} = $width if defined $width ;
144 20         52 return JSON::JSONFold::Config::config($preset, %overrides) ;
145             }
146              
147             sub _stream {
148 19     19   33 my ($fp, $config, $close_fp) = @_ ;
149 19         92 return JSON::JSONFold::Writer->new($fp, $config, $close_fp) ;
150             }
151              
152             sub _json_coder {
153 19     19   29 my ($gold, $indent, %opt) = @_;
154             # Must have valid indent, otherwise cannot parse the data
155 19         78 my $json = JSON::PP->new->pretty ;
156 19 50       1382 if ( $gold ) {
157 19   100     54 my $sort_keys = $opt{sort_keys} // 1 ;
158 19   50     69 $indent //=2 ;
159 19         240 $json->allow_nonref->canonical($sort_keys);
160 19         477 $json->space_before(0)->space_after(1);
161             }
162 19 50       335 $json->indent_length($indent) if defined $indent ;
163 19         139 return $json;
164             }
165              
166             # JSON compatible API - OO
167              
168             sub encode {
169 0     0 1 0 my ($self, $data) = @_ ;
170 0         0 return $self->format($data) ;
171             }
172              
173              
174             # JSON compatiable API - Functional
175              
176             sub encode_json {
177 18     18 1 385889 my ($data, $opts) = @_ ;
178 18 100       73 my %overrides = %$opts if $opts ;
179 18         32 my $width = delete $overrides{width} ;
180 18         25 my $compact = delete $overrides{compact} ;
181              
182 18         111 my $fmt = __PACKAGE__->new(width => $width, config => $compact, %overrides) ;
183 18         46 my $output = $fmt->format($data) ;
184 18         99 return $output ;
185             }
186              
187             # Same as encode_json - for users of legacy "JSON" wrapper.
188              
189             sub to_json {
190 0     0 1 0 my ($data, $opts) = @_ ;
191 0 0       0 my %overrides = %$opts if $opts ;
192 0         0 my $width = delete $overrides{width} ;
193 0         0 my $compact = delete $overrides{compact} ;
194              
195 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $compact, %overrides) ;
196 0         0 my $output = $fmt->format($data) ;
197 0         0 return $output ;
198             }
199            
200             sub run {
201 0     0 0 0 JSON::JSONFold::CLI::run() ;
202             }
203              
204             package JSON::JSONFold::Kind;
205              
206 5     5   33 use strict ;
  5         6  
  5         113  
207 5     5   16 use warnings ;
  5         6  
  5         218  
208 5     5   22 use Exporter 'import';
  5         7  
  5         252  
209              
210             our @EXPORT_OK = qw(
211             KIND_NONE
212             KIND_DICT
213             KIND_LIST
214             %OPENING_KIND
215             %CLOSING_KIND
216             );
217              
218 5     5   23 use constant KIND_NONE => 0;
  5         11  
  5         318  
219 5     5   40 use constant KIND_DICT => 1;
  5         13  
  5         192  
220 5     5   15 use constant KIND_LIST => 2;
  5         10  
  5         388  
221              
222             our %OPENING_KIND = (
223             '{' => KIND_DICT,
224             '[' => KIND_LIST,
225             );
226              
227             our %CLOSING_KIND = (
228             '}' => KIND_DICT,
229             '},' => KIND_DICT,
230             ']' => KIND_LIST,
231             '],' => KIND_LIST,
232             );
233              
234             # -------------------------------------------------------------------------
235             # Internal package: immutable-ish configuration record
236             # -------------------------------------------------------------------------
237              
238             package JSON::JSONFold::Config;
239              
240 5     5   20 use strict;
  5         5  
  5         88  
241 5     5   18 use warnings;
  5         5  
  5         204  
242 5     5   16 use Exporter 'import';
  5         6  
  5         153  
243              
244 5     5   32 use constant DEFAULT_WIDTH => 100 ;
  5         8  
  5         206  
245 5     5   19 use constant MAX_ARRAY_ITEMS => 1000 ;
  5         13  
  5         1616  
246 5     5   19 use constant MAX_OBJ_ITEMS => 1000 ;
  5         1638  
  5         209  
247 5     5   17 use constant MAX_NESTING => 10 ;
  5         10  
  5         1797  
248 5     5   21 use constant MAX_GRID_LINES => 1000 ;
  5         5  
  5         133  
249 5     5   16 use constant MAX_WIDTH => 255 ;
  5         5  
  5         491  
250              
251             our $SEQ = 0 ;
252             use constant {
253 5         826 C_OFF => $SEQ++,
254             C_WIDTH => $SEQ++,
255              
256             C_PACK_ARRAY_ITEMS => $SEQ++,
257             C_PACK_OBJ_ITEMS => $SEQ++,
258             C_PACK_NESTING => $SEQ++,
259              
260             C_FOLD_ARRAY_ITEMS => $SEQ++,
261             C_FOLD_OBJ_ITEMS => $SEQ++,
262             C_FOLD_NESTING => $SEQ++,
263              
264             C_GRID_ARRAY_ITEMS => $SEQ++,
265             C_GRID_OBJ_ITEMS => $SEQ++,
266             C_GRID_MIN_LINES => $SEQ++,
267             C_GRID_MAX_LINES => $SEQ++,
268             C_GRID_ARRAY_MIN => $SEQ++,
269             C_GRID_OBJ_MIN => $SEQ++,
270              
271             C_JOIN_ARRAY_ITEMS => $SEQ++,
272             C_JOIN_OBJ_ITEMS => $SEQ++,
273             C_JOIN_NESTING => $SEQ++,
274             C_UNUSED_LAST => $SEQ++,
275 5     5   18 } ;
  5         7  
276              
277             BEGIN {
278              
279 5     5   4445 our @EXPORT = qw(
280             C_OFF
281             C_WIDTH
282             C_PACK_ARRAY_ITEMS C_PACK_OBJ_ITEMS C_PACK_NESTING
283              
284             C_FOLD_ARRAY_ITEMS C_FOLD_OBJ_ITEMS C_FOLD_NESTING
285              
286             C_GRID_ARRAY_ITEMS C_GRID_OBJ_ITEMS C_GRID_MIN_LINES
287             C_GRID_MAX_LINES C_GRID_ARRAY_MIN C_GRID_OBJ_MIN
288              
289             C_JOIN_ARRAY_ITEMS C_JOIN_OBJ_ITEMS C_JOIN_NESTING
290             ) ;
291             }
292              
293             our @FIELDS = (
294             [ 'off', C_OFF ],
295             [ 'width', C_WIDTH ],
296             [ 'pack_array_items', C_PACK_ARRAY_ITEMS ],
297             [ 'pack_obj_items', C_PACK_OBJ_ITEMS ],
298             [ 'pack_nesting', C_PACK_NESTING ],
299             [ 'fold_array_items', C_FOLD_ARRAY_ITEMS ],
300             [ 'fold_obj_items', C_FOLD_OBJ_ITEMS ],
301             [ 'fold_nesting', C_FOLD_NESTING ],
302             [ grid_array_items => C_GRID_ARRAY_ITEMS ],
303             [ grid_obj_items => C_GRID_OBJ_ITEMS ],
304             [ grid_min_lines => C_GRID_MIN_LINES ],
305             [ grid_max_lines => C_GRID_MAX_LINES ],
306             [ grid_array_min => C_GRID_ARRAY_MIN ],
307             [ grid_obj_min => C_GRID_OBJ_MIN ],
308             [ 'join_array_items', C_JOIN_ARRAY_ITEMS ],
309             [ 'join_obj_items', C_JOIN_OBJ_ITEMS ],
310             [ 'join_nesting', C_JOIN_NESTING ],
311             ) ;
312              
313             our %NAME_TO_INDEX = map { @$_ } @FIELDS ;
314             our %PRESETS ;
315             our ($NONE, $DEFAULT) ;
316              
317             sub as_hash {
318 0     0   0 my ($self) = @_ ;
319 0         0 map { my ($name, $idx) = @$_ ; ($name => $self->[$idx]) ; } @FIELDS ;
  0         0  
  0         0  
320             }
321              
322             sub _make {
323 15     15   51 my ($class, %arg) = @_;
324 15         18 my @d;
325 15         52 $#d = $SEQ;
326 15         21 $d[C_OFF] = $arg{off} ;
327 15         19 $d[C_WIDTH] = $arg{width};
328              
329 15         16 $d[C_PACK_ARRAY_ITEMS] = $arg{pack_array_items};
330 15         35 $d[C_PACK_OBJ_ITEMS] = $arg{pack_obj_items};
331 15         18 $d[C_PACK_NESTING] = $arg{pack_nesting};
332              
333 15         19 $d[C_FOLD_ARRAY_ITEMS] = $arg{fold_array_items};
334 15         20 $d[C_FOLD_OBJ_ITEMS] = $arg{fold_obj_items};
335 15         16 $d[C_FOLD_NESTING] = $arg{fold_nesting};
336              
337 15         19 $d[C_GRID_ARRAY_ITEMS] = $arg{grid_array_items} ;
338 15         17 $d[C_GRID_OBJ_ITEMS] = $arg{grid_obj_items} ;
339 15         17 $d[C_GRID_MIN_LINES] = $arg{grid_min_lines} ;
340 15         16 $d[C_GRID_MAX_LINES] = $arg{grid_max_lines} ;
341 15         21 $d[C_GRID_ARRAY_MIN] = $arg{grid_array_min} ;
342 15         13 $d[C_GRID_OBJ_MIN] = $arg{grid_obj_min} ;
343              
344 15         18 $d[C_JOIN_ARRAY_ITEMS] = $arg{join_array_items};
345 15         25 $d[C_JOIN_OBJ_ITEMS] = $arg{join_obj_items};
346 15         16 $d[C_JOIN_NESTING] = $arg{join_nesting};
347 15         53 return bless \@d, $class;
348             }
349              
350             sub _replace {
351 84     84   148 my ($base, $validate) = (shift, shift) ;
352 84 50       187 return $base unless @_ ;
353 84 100 66     252 my $overrides = @_ == 1 && ref($_[0]) ? $_[0] : { @_ } ;
354 84 100       289 return $base unless %$overrides ;
355              
356 46         153 my @d = @$base ;
357 46         84 for my $key (keys %$overrides) {
358 276 100       337 unless (exists $NAME_TO_INDEX{$key}) {
359 1 50       3 die "unknown JSON::JSONFold config key: $key\n" if $validate ;
360 1         2 next ;
361             }
362 275         333 $d[$NAME_TO_INDEX{$key}] = $overrides->{$key} ;
363             }
364 46   50     253 return bless \@d, ref($base) || __PACKAGE__ ;
365             }
366              
367             sub _resolve_config {
368 39     39   51 my ($config) = @_ ;
369 39 100       81 return $config if ref($config) ;
370              
371 19   100     38 my $name = $config // '' ;
372             die "unknown JSON::JSONFold preset: $name\n"
373 19 50       43 unless exists $PRESETS{$name} ;
374 19         48 return $PRESETS{$name} ;
375             }
376              
377             sub config {
378 39     39   59 my ($preset, %overrides) = @_ ;
379 39         62 return _replace(_resolve_config($preset), 0, \%overrides) ;
380             }
381              
382             sub new {
383 0     0   0 my ($class, $config, @args) = @_ ;
384 0         0 return config($config, @args) ;
385             }
386              
387             sub _new_preset {
388 45     45   48 my $base = shift ;
389 45         91 return _replace($base, 1, @_) ;
390             }
391              
392             sub _class_init {
393 5     5   8 my $class = shift ;
394              
395 5         19 $DEFAULT = $class->_make(
396             width => DEFAULT_WIDTH,
397              
398             pack_array_items => 10,
399             pack_obj_items => 5,
400             pack_nesting => 1,
401              
402             fold_array_items => 10,
403             fold_obj_items => 5,
404             fold_nesting => 2,
405              
406             grid_array_items => MAX_ARRAY_ITEMS,
407             grid_obj_items => MAX_OBJ_ITEMS,
408             grid_min_lines => 3,
409             grid_max_lines => 100,
410             grid_array_min => 3,
411             grid_obj_min => 3,
412              
413             join_array_items => 8,
414             join_obj_items => 4,
415             join_nesting => 1,
416             ) ;
417              
418 5         13 $NONE = $class->_make(
419             width => DEFAULT_WIDTH,
420              
421             pack_array_items => 0,
422             pack_obj_items => 0,
423             pack_nesting => 0,
424              
425             fold_array_items => 0,
426             fold_obj_items => 0,
427             fold_nesting => 0,
428              
429             grid_array_items => 0,
430             grid_obj_items => 0,
431             grid_min_lines => 0,
432             grid_max_lines => 0,
433             grid_array_min => 0,
434             grid_obj_min => 0,
435              
436             join_array_items => 0,
437             join_obj_items => 0,
438             join_nesting => 0,
439             ) ;
440              
441 5         12 my %pack_max = (
442             pack_array_items => MAX_ARRAY_ITEMS,
443             pack_obj_items => MAX_OBJ_ITEMS,
444             pack_nesting => MAX_NESTING,
445             ) ;
446 5         16 my %fold_max = (
447             fold_array_items => MAX_ARRAY_ITEMS,
448             fold_obj_items => MAX_OBJ_ITEMS,
449             fold_nesting => MAX_NESTING,
450             ) ;
451 5         11 my %join_max = (
452             join_array_items => MAX_ARRAY_ITEMS,
453             join_obj_items => MAX_OBJ_ITEMS,
454             join_nesting => MAX_NESTING,
455             ) ;
456 5         25 my %grid_max = (
457             grid_array_items => MAX_ARRAY_ITEMS,
458             grid_obj_items => MAX_OBJ_ITEMS,
459             grid_min_lines => 3,
460             grid_max_lines => MAX_GRID_LINES,
461             ) ;
462              
463 5         13 %PRESETS = (
464             off => $class->_make(off => 1),
465             '' => $DEFAULT,
466             default => $DEFAULT,
467             none => $NONE,
468              
469             low => _new_preset($DEFAULT,
470             fold_nesting => 0,
471             join_nesting => 0,
472             grid_max_lines => 0,
473             ),
474             med => _new_preset($DEFAULT,
475             join_nesting => 0,
476             grid_max_lines => 0,
477             ),
478             classic => _new_preset($DEFAULT,
479             grid_max_lines => 0,
480             ),
481             high => _new_preset($DEFAULT,
482             pack_array_items => 20, pack_obj_items => 10, pack_nesting => 4,
483             fold_array_items => 20, fold_obj_items => 10, fold_nesting => 4,
484             grid_array_min => 4, grid_obj_min => 4,
485             join_array_items => 16, join_obj_items => 8, join_nesting => 2,
486             ),
487             max => _new_preset($DEFAULT,
488             width => MAX_WIDTH,
489             %pack_max, %fold_max, %join_max, %grid_max,
490             grid_array_min => 4,
491             grid_obj_min => 4,
492             ),
493             pack => _new_preset($NONE, %pack_max),
494             fold => _new_preset($NONE, %fold_max),
495             grid => _new_preset($NONE, %pack_max, %fold_max, %grid_max),
496             join => _new_preset($NONE,
497             %fold_max,
498             join_array_items => MAX_ARRAY_ITEMS,
499             join_obj_items => MAX_OBJ_ITEMS,
500             join_nesting => MAX_NESTING,
501             ),
502             ) ;
503             }
504              
505             __PACKAGE__->_class_init ;
506              
507             # -------------------------------------------------------------------------
508             # Internal package: one physical pretty-printed line
509             # -------------------------------------------------------------------------
510              
511             package JSON::JSONFold::Line ;
512              
513 5     5   43 use strict ;
  5         32  
  5         134  
514 5     5   19 use warnings ;
  5         17  
  5         241  
515 5     5   20 use Exporter 'import' ;
  5         6  
  5         200  
516              
517 5     5   17 use constant KIND_NONE => $JSON::JSONFold::Kind::KIND_NONE ;
  5         4  
  5         264  
518 5     5   17 use constant KIND_DICT => $JSON::JSONFold::Kind::KIND_DICT ;
  5         5  
  5         207  
519 5     5   16 use constant KIND_LIST => $JSON::JSONFold::Kind::KIND_LIST ;
  5         11  
  5         469  
520              
521             our $SEQ = 0 ;
522             use constant {
523 5         525 L_INDENT => $SEQ++,
524             L_PARTS => $SEQ++,
525             L_PARTS_LENGTH => $SEQ++,
526             L_KIND => $SEQ++,
527             L_ITEMS => $SEQ++,
528             L_LEAFS => $SEQ++,
529             L_CHILD_NESTING => $SEQ++,
530             L_OPENER => $SEQ++,
531             L_CLOSER => $SEQ++,
532             L_CAN_JOIN => $SEQ++,
533             L_CAN_PACK => $SEQ++,
534             L_CAN_GRID => $SEQ++,
535 5     5   25 } ;
  5         5  
536              
537             BEGIN {
538 5     5   4991 our @EXPORT = qw(
539             L_INDENT
540             L_PARTS
541             L_PARTS_LENGTH
542             L_KIND
543             L_ITEMS
544             L_LEAFS
545             L_CHILD_NESTING
546             L_OPENER
547             L_CLOSER
548             L_CAN_JOIN
549             L_CAN_PACK
550             L_CAN_GRID
551             ) ;
552             }
553              
554             my $KEY_RE = qr/^\s*(?:(?:"[^"\\]*")|(?:'[^'\\]*')|(?:[A-Za-z_\$][A-Za-z0-9_\$]*)|)\s*:/ ;
555              
556             sub _calc_parts_length {
557 0     0   0 my ($parts) = @_ ;
558 0 0       0 return 0 unless @$parts ;
559 0         0 my $n = -1 ;
560 0         0 $n += 1 + length($_) for @$parts ;
561 0         0 return $n ;
562             }
563              
564             sub parse {
565 169     169   230 my ($class, $s) = @_ ;
566              
567 169         432 my ($spaces) = $s =~ /^(\s*)/;
568 169         292 my $body = substr($s, length($spaces));
569 169         273 $body =~ s/\s+\z//;
570              
571 169 50       275 my $last = length($body) ? substr($body, -1, 1) : '' ;
572 169   100     356 my $opener = $JSON::JSONFold::Kind::OPENING_KIND{$last} // KIND_NONE ;
573 169   100     306 my $closer = $JSON::JSONFold::Kind::CLOSING_KIND{$body} // KIND_NONE ;
574 169 100 100     329 my $is_body = !$opener && !$closer ? 1 : 0;
575              
576 169         161 my @d ;
577 169         309 $#d = $SEQ ;
578 169         210 $d[L_INDENT] = length($spaces);
579            
580 169         245 $d[L_PARTS] = [ $body ] ;
581 169         165 $d[L_PARTS_LENGTH] = length($body) ;
582 169         148 $d[L_KIND] = KIND_NONE ;
583 169 100       271 $d[L_ITEMS] = $is_body ? 1 : 0 ;
584 169 100       231 $d[L_LEAFS] = $is_body ? 1 : 0 ;
585 169         183 $d[L_CHILD_NESTING] = -1;
586 169         166 $d[L_OPENER] = $opener;
587 169         234 $d[L_CLOSER] = $closer;
588 169         172 $d[L_CAN_JOIN] = $is_body;
589 169         167 $d[L_CAN_PACK] = $is_body;
590 169         167 $d[L_CAN_GRID] = 0 ;
591              
592 169         358 return bless \@d, $class ;
593             }
594              
595             sub raw {
596 60     60   124 return (' ' x $_[0][L_INDENT]) . join(' ', @{ $_[0][L_PARTS] }) . "\n"
  60         156  
597             }
598              
599             sub width {
600 106     106   221 return $_[0][L_INDENT] + $_[0][L_PARTS_LENGTH]
601             }
602              
603             sub can_merge {
604 45     45   64 my ($self, $other, $item_limit, $width_limit) = @_ ;
605 45   33     209 return $self->[L_INDENT] == $other->[L_INDENT]
606             && $self->[L_ITEMS] + $other->[L_ITEMS] <= $item_limit
607             && $self->[L_INDENT] + $self->[L_PARTS_LENGTH] + 1 + $other->[L_PARTS_LENGTH] <= $width_limit ;
608             }
609              
610             sub merge_line {
611 45     45   48 my ($self, $other) = @_;
612 45         49 push @{ $self->[L_PARTS] }, @{ $other->[L_PARTS] } ;
  45         52  
  45         78  
613 45 50       47 $self->[L_PARTS_LENGTH] += 1 + $other->[L_PARTS_LENGTH] if @{ $other->[L_PARTS] } ;
  45         91  
614 45         49 $self->[L_ITEMS] += $other->[L_ITEMS];
615 45         44 $self->[L_LEAFS] += $other->[L_LEAFS];
616 45 50       67 if ($other->[L_CHILD_NESTING] > $self->[L_CHILD_NESTING]) {
617 0         0 $self->[L_CHILD_NESTING] = $other->[L_CHILD_NESTING];
618 0         0 $self->[L_CAN_PACK] = 0;
619             }
620 45         46 return $self;
621             }
622              
623             sub set_parts {
624 0     0   0 my ($self, $parts) = @_ ;
625 0         0 $self->[L_PARTS] = $parts ;
626 0         0 $self->[L_PARTS_LENGTH] = _calc_parts_length($parts) ;
627 0         0 return $self ;
628             }
629              
630             sub dict_signature {
631 0     0   0 my ($self) = @_ ;
632 0         0 my @parts = @{ $self->[L_PARTS] } ;
  0         0  
633 0 0       0 return undef if @parts < 3 ;
634              
635 0         0 my @signature ;
636 0         0 for my $part (@parts[1 .. $#parts - 1]) {
637 0 0       0 return undef unless $part =~ /($KEY_RE)/ ;
638 0         0 push @signature, $1 ;
639             }
640 0         0 return join("\x1e", @signature) ;
641             }
642              
643             sub _format_parts {
644 0     0   0 my ($parts, $widths) = @_ ;
645 0         0 my $last = $#$widths ;
646 0         0 my @out ;
647 0         0 for my $i (0 .. $#$parts) {
648 0         0 my $part = $parts->[$i] ;
649 0         0 my $w = $widths->[$i] ;
650 0 0       0 if ($part =~ /^[\-0-9]/) {
    0          
651 0         0 push @out, sprintf("%*s", $w, $part) ;
652             }
653             elsif ($i < $last) {
654 0         0 push @out, sprintf("%-*s", $w, $part) ;
655             }
656             else {
657 0         0 push @out, $part ;
658             }
659             }
660 0         0 return \@out ;
661             }
662              
663             sub apply_grid {
664 0     0   0 my ($self, $widths) = @_ ;
665 0         0 return $self->set_parts(_format_parts($self->[L_PARTS], $widths)) ;
666             }
667              
668             # -------------------------------------------------------------------------
669             # Internal package: stack frame for a currently open JSON container
670             # -------------------------------------------------------------------------
671              
672              
673              
674             package JSON::JSONFold::Frame;
675 5     5   44 use strict;
  5         9  
  5         112  
676 5     5   16 use warnings;
  5         6  
  5         237  
677 5     5   26 use Exporter 'import' ;
  5         6  
  5         166  
678              
679             BEGIN {
680 5     5   701 JSON::JSONFold::Line->import() ;
681             }
682              
683             our $SEQ = 0 ;
684             use constant {
685 5         687 F_KIND => $SEQ++,
686             F_INDENT => $SEQ++,
687             F_DEPTH => $SEQ++,
688             F_LINES => $SEQ++,
689             F_PARTS_LENGTH => $SEQ++,
690             F_PACK_LIMIT => $SEQ++,
691             F_FOLD_LIMIT => $SEQ++,
692             F_JOIN_LIMIT => $SEQ++,
693             F_GRID_LIMIT => $SEQ++,
694             F_GRID_MIN_ITEMS => $SEQ++,
695             F_CONTENT_LINES => $SEQ++,
696             F_ITEMS => $SEQ++,
697             F_LEAFS => $SEQ++,
698             F_FOLD_OK => $SEQ++,
699             F_GRID_OK => $SEQ++,
700             F_CHILD_NESTING => $SEQ++,
701 5     5   21 } ;
  5         8  
702              
703             BEGIN {
704 5     5   3935 our @EXPORT = qw(
705             F_KIND F_INDENT F_DEPTH F_LINES F_PARTS_LENGTH
706             F_PACK_LIMIT F_FOLD_LIMIT F_JOIN_LIMIT F_GRID_LIMIT F_GRID_MIN_ITEMS
707             F_CONTENT_LINES F_ITEMS F_LEAFS F_FOLD_OK F_GRID_OK F_CHILD_NESTING
708             ) ;
709             }
710              
711             sub new {
712 47     47   163 my ($class, %arg) = @_;
713 47         77 my @d ;
714 47         76 $#d = $SEQ ;
715 47   50     81 $d[F_KIND] = $arg{kind} // 0 ;
716 47   50     82 $d[F_INDENT] = $arg{indent} // 0 ;
717 47   50     60 $d[F_DEPTH] = $arg{depth} // 0;
718 47   50     102 $d[F_LINES] = $arg{lines} // [];
719 47         52 $d[F_PARTS_LENGTH] = 0 ;
720 47   50     79 $d[F_PACK_LIMIT] = $arg{pack_limit} // 0;
721 47   50     66 $d[F_FOLD_LIMIT] = $arg{fold_limit} // 0;
722 47   50     73 $d[F_JOIN_LIMIT] = $arg{join_limit} // 0;
723 47   50     69 $d[F_GRID_LIMIT] = $arg{grid_limit} // 0 ;
724 47   50     70 $d[F_GRID_MIN_ITEMS] = $arg{grid_min_items} // 0 ;
725 47         68 $d[F_CONTENT_LINES] = 0;
726 47         72 $d[F_ITEMS] = 0;
727 47         49 $d[F_LEAFS] = 0;
728 47         59 $d[F_FOLD_OK] = 1;
729 47         67 $d[F_GRID_OK] = 0 ;
730 47         56 $d[F_CHILD_NESTING] = -1;
731 47         94 return bless \@d, $class;
732             }
733              
734 124     124   124 sub is_empty { return @{ $_[0][F_LINES] } == 0 }
  124         259  
735 90     90   99 sub last_line { return $_[0][F_LINES][-1] }
736              
737             sub update_stats {
738 205     205   215 my ($self, $line) = @_ ;
739 205         208 $self->[F_ITEMS] += $line->[L_ITEMS];
740 205         212 $self->[F_LEAFS] += $line->[L_LEAFS];
741 205 100       274 $self->[F_PARTS_LENGTH] += $line->[L_PARTS_LENGTH] + ($self->[F_PARTS_LENGTH] ? 1 : 0) ;
742 205 100       270 if ($line->[L_CHILD_NESTING] >= $self->[F_CHILD_NESTING]) {
743 61         68 $self->[F_CHILD_NESTING] = $line->[L_CHILD_NESTING] + 1;
744             }
745 205         205 return ;
746             }
747              
748             sub add_line {
749 166     166   191 my ($self, $line) = @_ ;
750 166         141 push @{ $self->[F_LINES] }, $line ;
  166         248  
751 166 100 100     379 if (!$line->[L_OPENER] && !$line->[L_CLOSER]) {
752 62         60 $self->[F_CONTENT_LINES]++ ;
753             }
754 166         278 $self->update_stats($line) ;
755 166         155 return ;
756             }
757              
758             sub check_fold_limits {
759 101     101   116 my ($self, $cfg) = @_ ;
760 101 50       156 return 0 if $self->[F_PARTS_LENGTH] > $cfg->[JSON::JSONFold::Config::C_WIDTH] ;
761 101 100       129 return 0 if $self->[F_ITEMS] > $self->[F_FOLD_LIMIT] ;
762 97 100       135 return 0 if $self->[F_CHILD_NESTING] >= $cfg->[JSON::JSONFold::Config::C_FOLD_NESTING] ;
763 92         152 return 1 ;
764             }
765              
766             sub fold_lines {
767 32     32   36 my ($self, $cfg) = @_ ;
768 32         36 my @parts = map { @{ $_->[L_PARTS] } } @{ $self->[F_LINES] } ;
  96         92  
  96         196  
  32         67  
769              
770 32         38 my @d ;
771 32         71 $#d = $JSON::JSONFold::Line::SEQ ;
772 32         39 $d[L_INDENT] = $self->[F_INDENT] ;
773 32         34 $d[L_PARTS] = \@parts ;
774 32         37 $d[L_PARTS_LENGTH] = $self->[F_PARTS_LENGTH] ;
775 32         34 $d[L_KIND] = $self->[F_KIND] ;
776 32         36 $d[L_ITEMS] = 1 ;
777 32         53 $d[L_LEAFS] = $self->[F_LEAFS] ;
778 32         36 $d[L_CHILD_NESTING] = $self->[F_CHILD_NESTING] ;
779 32         34 $d[L_OPENER] = JSON::JSONFold::Kind::KIND_NONE ;
780 32         36 $d[L_CLOSER] = JSON::JSONFold::Kind::KIND_NONE ;
781 32         45 $d[L_CAN_PACK] = 0 ;
782 32 100       55 $d[L_CAN_JOIN] = $self->[F_CHILD_NESTING] < $cfg->[JSON::JSONFold::Config::C_JOIN_NESTING] ? 1 : 0 ;
783 32 100 66     82 $d[L_CAN_GRID] = ($cfg->[JSON::JSONFold::Config::C_GRID_MAX_LINES] > 0
784             && $self->[F_ITEMS] <= $self->[F_GRID_LIMIT]) ? 1 : 0 ;
785              
786 32         51 @{ $self->[F_LINES] } = (bless \@d, 'JSON::JSONFold::Line') ;
  32         116  
787 32         52 return ;
788             }
789              
790             sub join_lines {
791 10     10   29 my ($self, $cfg) = @_ ;
792 10         13 my $lines = $self->[F_LINES] ;
793 10         15 my $n = @$lines ;
794 10 50       22 return if $n < 2 ;
795              
796 10         12 my $prev = $lines->[0] ;
797 10         12 my $write_pos = 1 ;
798              
799 10         33 for (my $read_pos = 1; $read_pos < $n; $read_pos++) {
800 26         36 my $line = $lines->[$read_pos] ;
801 26 100 100     87 if ($prev->[L_CAN_JOIN]
      66        
802             && $line->[L_CAN_JOIN]
803             && $prev->can_merge($line, $self->[F_JOIN_LIMIT], $cfg->[JSON::JSONFold::Config::C_WIDTH])) {
804 6         21 $prev->merge_line($line) ;
805 6         17 $prev->[L_CAN_PACK] = 0 ;
806             }
807             else {
808 20 100       59 $lines->[$write_pos] = $line if $read_pos != $write_pos ;
809 20         24 $prev = $line ;
810 20         32 $write_pos++ ;
811             }
812             }
813              
814 10 100       27 splice(@$lines, $write_pos) if $write_pos < @$lines ;
815 10         25 $self->[F_CONTENT_LINES] -= ($n - $write_pos) ;
816 10         30 return ;
817             }
818              
819             # -------------------------------------------------------------------------
820             # Internal package: counters
821             # -------------------------------------------------------------------------
822              
823             package JSON::JSONFold::Stats;
824 5     5   33 use strict;
  5         7  
  5         83  
825 5     5   13 use warnings;
  5         6  
  5         926  
826              
827             sub new {
828 19     19   29 my ($class) = @_;
829 19         103 return bless {
830             bytes_in => 0,
831             bytes_out => 0,
832             lines_in => 0,
833             lines_out => 0,
834             }, $class;
835             }
836              
837 0     0   0 sub bytes_in { $_[0]{bytes_in} }
838 0     0   0 sub bytes_out { $_[0]{bytes_out} }
839 0     0   0 sub lines_in { $_[0]{lines_in} }
840 0     0   0 sub lines_out { $_[0]{lines_out} }
841              
842 0     0   0 sub as_hash { return %{ $_[0] } }
  0         0  
843              
844             # -------------------------------------------------------------------------
845             # Internal package: streaming folding filter/writer
846             # -------------------------------------------------------------------------
847              
848             package JSON::JSONFold::Writer;
849 5     5   29 use strict;
  5         6  
  5         171  
850 5     5   22 use warnings;
  5         6  
  5         238  
851              
852             BEGIN {
853 5     5   216 JSON::JSONFold::Line->import() ;
854 5         209 JSON::JSONFold::Frame->import() ;
855 5         528 JSON::JSONFold::Config->import() ;
856             }
857              
858             our $SEQ = 0 ;
859             use constant {
860 5         11789 W_UNUSED_FIRST => $SEQ++,
861             W_FH => $SEQ++,
862             W_CFG => $SEQ++,
863             W_PENDING => $SEQ++,
864             W_STACK => $SEQ++,
865             W_STATS => $SEQ++,
866             W_DO_CLOSE => $SEQ++,
867             W_UNUSED_LAST => $SEQ++,
868 5     5   29 } ;
  5         5  
869              
870             sub new {
871 19     19   35 my ($class, $fh, $config, $do_close) = @_;
872              
873 19         28 my $cfg = JSON::JSONFold::Config::config($config) ;
874 19         27 my @d ;
875 19         55 $#d = $SEQ ;
876 19         49 $d[W_FH] = $fh;
877 19 100       56 $d[W_CFG] = $cfg unless $cfg->[C_OFF] ;
878 19         26 $d[W_PENDING] = '';
879 19         36 $d[W_STACK] = [];
880 19         83 $d[W_STATS] = JSON::JSONFold::Stats->new;
881 19         30 $d[W_DO_CLOSE] = $do_close;
882 19         41 return bless \@d, $class;
883             }
884              
885 1     1   4 sub stats { return $_[0][W_STATS] }
886              
887             sub write {
888 19     19   37 my ($self, $s) = @_;
889 19 50       36 $s = '' unless defined $s;
890 19         25 my $len = length($s);
891 19         48 $self->[W_STATS]{bytes_in} += $len;
892              
893 19 100       43 unless ($self->[W_CFG]) {
894 1         4 $self->[W_STATS]{lines_in} += _count_newlines($s);
895 1         4 return $self->_write_str($s);
896             }
897              
898 18         33 my $nl = index($s, "\n");
899 18 50       33 if ($nl < 0) {
900 0         0 $self->[W_PENDING] .= $s;
901 0         0 return $len;
902             }
903              
904 18         34 my $nl2 = index($s, "\n", $nl + 1);
905 18 100       41 if ($nl2 < 0) {
906 2         3 $self->[W_STATS]{lines_in}++;
907 2         5 my $line_text = $self->[W_PENDING] . substr($s, 0, $nl);
908 2         6 $self->[W_PENDING] = substr($s, $nl + 1);
909 2         9 $self->_feed(JSON::JSONFold::Line->parse($line_text));
910 2         11 return $len;
911             }
912              
913             # We have multiple lines - at least 2 new lines in the new buffer
914 16         71 my @lines = split("\n", $s, -1) ;
915 16         42 $lines[0] = $self->[W_PENDING] . $lines[0] ;
916 16         25 $self->[W_PENDING] = pop @lines ;
917 16         47 for my $part ( @lines ) {
918 167         313 $self->_feed(JSON::JSONFold::Line->parse($part));
919              
920             }
921 16         24 $self->[W_STATS]{lines_in} += @lines;
922              
923 16         39 return $len;
924             }
925              
926             sub finish {
927 2     2   6 my ($self) = @_;
928 2 50       7 if (length $self->[W_PENDING]) {
929 0         0 $self->_feed(JSON::JSONFold::Line->parse($self->[W_PENDING], $self->_parent_kind));
930 0         0 $self->[W_PENDING] = '';
931             }
932              
933 2         4 for my $frame (@{ $self->[W_STACK] }) {
  2         8  
934 0         0 $self->_write_line($_) for @{ $frame->[F_LINES] };
  0         0  
935             }
936 2         4 @{ $self->[W_STACK] } = ();
  2         6  
937             }
938              
939             sub flush {
940 2     2   5 my ($self) = @_;
941 2         5 my $fh = $self->[W_FH];
942 2 50 33     53 $fh->flush if $fh && $fh->can('flush');
943             }
944              
945             sub close {
946 1     1   4 my ($self) = @_;
947 1         4 $self->finish;
948 1         4 $self->flush;
949 1 50       5 $self->[W_FH]->close if $self->[W_DO_CLOSE] ;
950             }
951              
952             sub _feed {
953 169     169   200 my ($self, $line) = @_;
954             # Opener
955 169 100       254 if ($line->[L_OPENER]) {
956             my $frame = JSON::JSONFold::Frame->new(
957             kind => $line->[L_OPENER],
958             indent => $line->[L_INDENT],
959 47         102 depth => scalar(@{ $self->[W_STACK] }),
  47         100  
960             pack_limit => $self->_pack_limit($line->[L_OPENER]),
961             fold_limit => $self->_fold_limit($line->[L_OPENER]),
962             join_limit => $self->_join_limit($line->[L_OPENER]),
963             grid_limit => $self->_grid_limit($line->[L_OPENER]),
964             grid_min_items => $self->_grid_min_items($line->[L_OPENER]),
965             ) ;
966 47         102 $frame->add_line($line) ;
967 47         45 push @{ $self->[W_STACK] }, $frame ;
  47         56  
968              
969 47 50       72 $self->_mark_no_fold if $line->width > $self->[W_CFG][C_WIDTH] ;
970 47         95 return ;
971             }
972              
973 122 100       106 unless (@{ $self->[W_STACK] }) {
  122         214  
974 2         6 $self->_write_line($line) ;
975 2         4 return ;
976             }
977              
978 120         130 my $frame = $self->[W_STACK][-1] ;
979              
980 120 100       169 if ($line->[L_CLOSER]) {
981 47 50       66 if ($frame->[F_KIND] != $line->[L_CLOSER]) {
982 0         0 $frame->[F_FOLD_OK] = 0 ;
983 0         0 $frame->[F_GRID_OK] = 0 ;
984             }
985 47         97 $frame->add_line($line) ;
986 47         79 $self->_close_frame ;
987 47         188 return ;
988             }
989              
990 73 100       103 $line->[L_CAN_PACK] = 0 if $line->[L_ITEMS] >= $frame->[F_PACK_LIMIT] ;
991 73 100       93 $line->[L_CAN_JOIN] = 0 if $line->[L_ITEMS] >= $frame->[F_JOIN_LIMIT] ;
992 73         135 $self->_add_to_frame($frame, $line) ;
993 73         145 return ;
994             }
995              
996             sub _emit_lines {
997 62     62   89 my ($self, $lines, $depth) = @_;
998 62 50       84 return unless @$lines;
999 62 100       85 $depth = @{ $self->[W_STACK] } - 1 unless defined $depth;
  47         64  
1000              
1001 62 100       95 if ($depth < 0) {
1002 24         50 $self->_write_line($_) for @$lines;
1003             return
1004 24         31 }
1005              
1006 38         47 my $frame = $self->[W_STACK][$depth];
1007 38         76 $self->_add_to_frame($frame, $_) for @$lines;
1008             return
1009 38         42 }
1010              
1011             sub _add_to_frame {
1012 124     124   203 my ($self, $frame, $line) = @_;
1013              
1014 124 100 66     170 if (!$frame->is_empty) {
    100          
1015 107 100       162 unless ($frame->[F_GRID_OK]) {
1016 90         110 my $prev = $frame->last_line ;
1017 90 100 100     266 return if $line->[L_CAN_PACK] && $prev->[L_CAN_PACK] && $self->_try_pack($frame, $prev, $line) ;
      66        
1018 55 100 100     122 return if $line->[L_CAN_JOIN] && $prev->[L_CAN_JOIN] && $self->_try_join($frame, $prev, $line) ;
      66        
1019             }
1020             # If frame is empty, may be it's in "streaming" mode, which
1021             # mean that lines that can not be packed/joined can be sent
1022             # directly to the output:
1023             } elsif (
1024             !$frame->[F_FOLD_OK] && !$line->[L_CAN_PACK] && !$line->[L_CAN_JOIN]
1025             ) {
1026 13         25 $self->_write_line($line);
1027 13         20 return;
1028             }
1029              
1030 72         155 $frame->add_line($line) ;
1031              
1032 72 50 66     128 if ( $frame->[F_FOLD_OK] && $line->width > $self->[W_CFG][C_WIDTH] ) {
1033 0         0 $self->_mark_no_fold ;
1034             }
1035              
1036 72 100       135 unless ($line->[L_CLOSER]) {
1037 67 100 100     124 if ($frame->[F_FOLD_OK] && !$frame->check_fold_limits($self->[W_CFG])) {
1038 8         18 $self->_mark_no_fold ;
1039             }
1040              
1041 67 50 66     118 if ($frame->[F_GRID_OK] && !$line->[L_CAN_GRID]) {
1042 0         0 $self->_mark_no_grid ;
1043 0         0 $frame->join_lines($self->[W_CFG]) ;
1044             }
1045             }
1046              
1047 72 100 100     143 $self->_stream_frame($frame) unless $frame->[F_FOLD_OK] || $frame->[F_GRID_OK] ;
1048 72         85 return ;
1049             }
1050              
1051             sub _merge_into_frame {
1052 39     39   50 my ($self, $frame, $prev, $line) = @_;
1053 39         88 $prev->merge_line($line) ;
1054              
1055 39 100 66     106 $prev->[L_CAN_PACK] = 0
1056             if $prev->[L_ITEMS] >= $frame->[F_PACK_LIMIT]
1057             || $prev->[L_CHILD_NESTING] >= $self->[W_CFG][C_PACK_NESTING] ;
1058              
1059 39 100 66     108 $prev->[L_CAN_JOIN] = 0
1060             if $prev->[L_ITEMS] >= $frame->[F_JOIN_LIMIT]
1061             || $prev->[L_CHILD_NESTING] >= $self->[W_CFG][C_JOIN_NESTING] ;
1062              
1063 39         74 $frame->update_stats($line) ;
1064              
1065 39 50 66     74 if ($frame->[F_FOLD_OK] && !$frame->check_fold_limits($self->[W_CFG])) {
1066 0         0 $self->_mark_no_fold ;
1067 0         0 $self->_stream_frame($frame) ;
1068             }
1069             }
1070              
1071             sub _try_pack {
1072 35     35   47 my ($self, $frame, $prev, $line) = @_ ;
1073 35 50       59 return 0 if $frame->[F_PACK_LIMIT] <= 1 ;
1074 35 50       65 return 0 unless $prev->can_merge($line, $frame->[F_PACK_LIMIT], $self->[W_CFG][C_WIDTH]) ;
1075 35         69 $self->_merge_into_frame($frame, $prev, $line);
1076 35 50       54 $prev->[L_CAN_JOIN] = 0 unless $prev->[L_CAN_PACK] ;
1077 35         74 return 1;
1078             }
1079              
1080             sub _try_grid {
1081 10     10   16 my ($self, $frame) = @_ ;
1082 10 100       30 return 0 if $frame->[F_KIND] != JSON::JSONFold::Kind::KIND_LIST ;
1083              
1084 1         2 my $line_count = @{ $frame->[F_LINES] } - 2 ;
  1         3  
1085 1 50 33     7 return 0 if $line_count < 2
      33        
1086             || $line_count < $self->[W_CFG][C_GRID_MIN_LINES]
1087             || $line_count > $self->[W_CFG][C_GRID_MAX_LINES] ;
1088              
1089 0         0 my @lines = @{ $frame->[F_LINES] }[1 .. @{ $frame->[F_LINES] } - 2] ;
  0         0  
  0         0  
1090 0 0       0 return 0 unless @lines ;
1091              
1092 0         0 my $first = $lines[0] ;
1093 0         0 my $part_count = @{ $first->[L_PARTS] } ;
  0         0  
1094 0 0 0     0 return 0 if $part_count < 4 || ($part_count - 2) < $frame->[F_GRID_MIN_ITEMS] ;
1095              
1096 0         0 for my $line (@lines) {
1097 0 0       0 return 0 if @{ $line->[L_PARTS] } != $part_count ;
  0         0  
1098             }
1099              
1100 0 0       0 if ($first->[L_KIND] == JSON::JSONFold::Kind::KIND_DICT) {
1101 0         0 my $sig = $first->dict_signature ;
1102 0 0       0 return 0 unless defined $sig ;
1103 0         0 for my $line (@lines) {
1104 0         0 my $line_sig = $line->dict_signature ;
1105 0 0 0     0 return 0 unless defined $line_sig && $line_sig eq $sig ;
1106             }
1107             }
1108              
1109 0         0 my @widths ;
1110 0         0 for my $i (0 .. $part_count - 1) {
1111 0         0 my $max = 0 ;
1112 0         0 for my $line (@lines) {
1113 0         0 my $len = length($line->[L_PARTS][$i]) ;
1114 0 0       0 $max = $len if $len > $max ;
1115             }
1116 0         0 push @widths, $max ;
1117             }
1118              
1119 0         0 my $grided_length = -1 ;
1120 0         0 $grided_length += 1 + $_ for @widths ;
1121 0 0       0 return 0 if $frame->[F_LINES][0][L_INDENT] + $grided_length > $self->[W_CFG][C_WIDTH] ;
1122              
1123 0         0 for my $line (@lines) {
1124 0         0 $line->apply_grid(\@widths) ;
1125 0         0 $line->[L_CAN_PACK] = 0 ;
1126 0         0 $line->[L_CAN_JOIN] = 0 ;
1127 0         0 $line->[L_CAN_GRID] = 0 ;
1128             }
1129 0         0 return 1 ;
1130             }
1131              
1132             sub _try_join {
1133 4     4   5 my ($self, $frame, $prev, $line) = @_;
1134 4 50       8 return 0 if $frame->[F_JOIN_LIMIT] <= 1 ;
1135 4 50       8 return 0 unless $prev->can_merge($line, $frame->[F_JOIN_LIMIT], $self->[W_CFG][C_WIDTH]) ;
1136 4         7 $self->_merge_into_frame($frame, $prev, $line);
1137 4         10 return 1;
1138             }
1139              
1140              
1141              
1142             sub _close_frame {
1143 47     47   64 my ($self) = @_ ;
1144              
1145 47         46 my $frame = pop @{ $self->[W_STACK] };
  47         64  
1146              
1147 47 100       75 if ($frame->[F_GRID_OK]) {
1148 10 50       21 if ($self->_try_grid($frame)) {
1149 0         0 $self->_mark_no_grid ;
1150             }
1151             else {
1152 10         25 $self->_mark_no_grid ;
1153 10         30 $frame->join_lines($self->[W_CFG]) ;
1154 10         20 $frame->[F_FOLD_OK] = $frame->check_fold_limits($self->[W_CFG]) ;
1155             }
1156             }
1157              
1158 47 100       64 if ($frame->[F_FOLD_OK]) {
1159 35 100       59 if ($self->_try_fold($frame)) {
1160 32 100 100     31 if (@{ $self->[W_STACK] } && $frame->[F_LINES][0][L_CAN_GRID]) {
  32         91  
1161 18         22 my $parent = $self->[W_STACK][-1] ;
1162 18 100       40 $parent->[F_GRID_OK] = 1 if $parent->[F_CONTENT_LINES] == 0 ;
1163             }
1164             }
1165             }
1166              
1167 47         96 $self->_emit_lines($frame->[F_LINES]) ;
1168 47         314 return ;
1169             }
1170              
1171             sub _try_fold {
1172 35     35   41 my ($self, $frame) = @_ ;
1173             return 0 if !$frame->[F_FOLD_OK]
1174             || $frame->[F_CONTENT_LINES] != 1
1175 35 50 66     118 || @{ $frame->[F_LINES] } != 3
  32   66     139  
      66        
1176             || $frame->[F_INDENT] + $frame->[F_PARTS_LENGTH] > $self->[W_CFG][C_WIDTH] ;
1177              
1178 32         66 $frame->fold_lines($self->[W_CFG]) ;
1179 32         71 return 1 ;
1180             }
1181              
1182             sub _stream_frame {
1183 19     19   27 my ($self, $frame) = @_;
1184 19         17 my $lines = $frame->[F_LINES];
1185 19 50       33 return unless @$lines ;
1186              
1187 19         22 my $last = $lines->[-1] ;
1188 19   66     36 my $keep_last = $last->[L_CAN_PACK] || $last->[L_CAN_JOIN] ;
1189 19 100       25 pop @$lines if $keep_last ;
1190 19 100       30 if ( @$lines ) {
1191 15         32 $self->_emit_lines($lines, $frame->[F_DEPTH] - 1) ;
1192 15         38 @$lines = ();
1193             }
1194 19 100       24 push @$lines, $last if $keep_last ;
1195             return
1196 19         22 }
1197              
1198             sub _mark_no_fold {
1199 8     8   12 my ($self) = @_;
1200 8         10 $_->[F_FOLD_OK] = 0 for @{ $self->[W_STACK] };
  8         22  
1201             }
1202              
1203             sub _mark_no_grid {
1204 10     10   28 my ($self) = @_ ;
1205 10         14 $_->[F_GRID_OK] = 0 for @{ $self->[W_STACK] } ;
  10         23  
1206 10         12 return ;
1207             }
1208              
1209             sub _write_line {
1210 60     60   71 my ($self, $line) = @_ ;
1211 60         88 $self->[W_STATS]{lines_out} ++ ;
1212 60         77 return $self->_write_str($line->raw) ;
1213             }
1214              
1215             sub _write_str {
1216 61     61   81 my ($self, $s) = @_;
1217              
1218 61         482 $self->[W_FH]->print($s) ;
1219 61         32489 $self->[W_STATS]{bytes_out} += length($s);
1220 61         96 return length($s);
1221             }
1222              
1223              
1224             sub _choose_limit {
1225 235     235   265 my ($kind, $list, $dict) = @_;
1226 235 50       556 return $kind == JSON::JSONFold::Kind::KIND_LIST() ? $list
    100          
1227             : $kind == JSON::JSONFold::Kind::KIND_DICT() ? $dict
1228             : 0;
1229             }
1230              
1231 47     47   87 sub _pack_limit { _choose_limit($_[1], $_[0][W_CFG][C_PACK_ARRAY_ITEMS], $_[0][W_CFG][C_PACK_OBJ_ITEMS]) }
1232 47     47   95 sub _fold_limit { _choose_limit($_[1], $_[0][W_CFG][C_FOLD_ARRAY_ITEMS], $_[0][W_CFG][C_FOLD_OBJ_ITEMS]) }
1233 47     47   72 sub _join_limit { _choose_limit($_[1], $_[0][W_CFG][C_JOIN_ARRAY_ITEMS], $_[0][W_CFG][C_JOIN_OBJ_ITEMS]) }
1234 47     47   74 sub _grid_limit { _choose_limit($_[1], $_[0][W_CFG][C_GRID_ARRAY_ITEMS], $_[0][W_CFG][C_GRID_OBJ_ITEMS]) }
1235 47     47   77 sub _grid_min_items { _choose_limit($_[1], $_[0][W_CFG][C_GRID_ARRAY_MIN], $_[0][W_CFG][C_GRID_OBJ_MIN]) }
1236 1     1   3 sub _count_newlines { return ($_[0] =~ tr/\n//) }
1237              
1238             # -------------------------------------------------------------------------
1239             # CLI
1240             # -------------------------------------------------------------------------
1241              
1242             package JSON::JSONFold::CLI ;
1243              
1244 5     5   32 use strict ;
  5         11  
  5         114  
1245 5     5   21 use warnings ;
  5         6  
  5         187  
1246 5     5   30 use Exporter 'import';
  5         15  
  5         4275  
1247              
1248             our @EXPORT_OK = qw(
1249             demo_data
1250             run
1251             ) ;
1252              
1253             sub setup {
1254 0     0     require Carp ;
1255              
1256             $SIG{__DIE__} = sub {
1257 0 0   0     return if $^S;
1258 0           local $SIG{__DIE__};
1259 0           Carp::confess(@_);
1260 0           };
1261              
1262             $SIG{__WARN__} = sub {
1263 0     0     local $SIG{__WARN__};
1264 0           Carp::cluck(@_);
1265 0           };
1266              
1267 0           require Getopt::Long ;
1268              
1269             }
1270              
1271             sub demo_data {
1272             return {
1273             meta => { version => 1, ok => JSON::PP::true, name => "jsonfold demo" },
1274             ids => [ 1, 2, 3, 4, 5, 6 ],
1275             items => [ { id => 1, name => "alpha" }, { id => 2, name => "beta" }, ],
1276             matrix => [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ],
1277             long => [
1278             "this is a long message that may force the block to stay expanded",
1279             "second",
1280             "third",
1281             "fourth",
1282             ],
1283             "single-array" => [1],
1284             "single-obj" => { x => 2 },
1285 0           long_array => [ map { "a$_" } 1..50 ],
1286 0           wide_array => [ map { "abcdefghijklmnopqrstuvwxyz$_" } 1 .. 9 ],
1287 0     0     wide_object => { map { ; "abcdefghijk$_" => "lmnopqrstuvwxyz$_" } 1 .. 9 },
  0            
1288              
1289             };
1290             }
1291              
1292             sub parse_options {
1293 0     0     my %opt = (
1294             compact => 'default',
1295             indent => 2,
1296             sort_keys => 1,
1297             );
1298              
1299             Getopt::Long::GetOptions(
1300             'demo' => \$opt{demo},
1301             'verbose|v' => \$opt{verbose},
1302             'help|h' => \$opt{help},
1303             'input|i=s' => \$opt{input},
1304             'compact=s' => \$opt{compact},
1305             'indent=i' => \$opt{indent},
1306             'sort-keys!' => \$opt{sort_keys},
1307 0           );
1308              
1309 0           return \%opt ;
1310             }
1311              
1312             sub usage {
1313 0     0     my $out = shift ;
1314 0           $out->print(<<___
1315             Usage: json-jsonfold [options] < input.json
1316              
1317             --demo
1318             --compact=default|classic|none|low|med|high|max|grid|pack|fold|join|off
1319             --width=N
1320             --indent=N
1321             --sort-keys
1322             --input=FILE
1323             ___
1324             ) ;
1325             }
1326              
1327             sub read_input {
1328 0     0     my ($input) = @_ ;
1329              
1330 0           my $json_text;
1331 0 0         if (defined $input) {
1332 0 0         open my $fh, '<', $input or die "$input: $!\n";
1333 0           local $/;
1334 0           $json_text = <$fh>;
1335 0 0         close $fh or die "$input: $!\n";
1336             } else {
1337 0           local $/;
1338 0           $json_text = ;
1339             }
1340              
1341 0           return JSON::PP->new->allow_nonref->decode($json_text);
1342             }
1343              
1344             sub show_verbose {
1345 0     0     require Data::Dumper ;
1346              
1347 0           my ($label) = shift ;
1348 0           my $dumper = new Data::Dumper([])->Terse(1)->Indent(1)->Sortkeys(1)->Pair('=')->Quotekeys(0) ;
1349              
1350 0           my $s = $dumper->Values( \@_)->Dump ;
1351 0           $s =~ s/\s+/ /gsm ;
1352              
1353 0           print STDERR "$label: $s\n" ;
1354              
1355             }
1356              
1357             sub run {
1358 0     0     setup() ;
1359 0           my $opt = parse_options();
1360              
1361 0 0         if ($opt->{help}) {
1362 0           usage(\*STDOUT);
1363 0           return 0;
1364             }
1365              
1366 0 0         my $data = $opt->{demo} ? demo_data() : read_input($opt->{input});
1367 0           my %cfg ;
1368 0           my $config = JSON::JSONFold::config($opt->{compact}, $opt->{width}, %cfg);
1369 0           my $verbose = $opt->{verbose} ;
1370              
1371 0 0         show_verbose("config", { $config->as_hash } ) if $verbose ;
1372            
1373 0           my $info = JSON::JSONFold::write_json($data, \*STDOUT, $opt->{width}, $config, sort_keys => $opt->{sort_keys});
1374              
1375 0 0         show_verbose("stats", { % $info }) if $verbose ;
1376 0           return 0;
1377             }
1378              
1379             run() unless caller() ;
1380              
1381             1;
1382              
1383             __END__