JSON-JSONFold
view release on metacpan or search on metacpan
lib/JSON/JSONFold.pm view on Meta::CPAN
) ;
}
sub new {
my ($class, %arg) = @_;
my @d ;
$#d = $SEQ ;
$d[F_KIND] = $arg{kind};
$d[F_DEPTH] = $arg{depth} // 0;
$d[F_LINES] = $arg{lines} || [];
$d[F_PACK_LIMIT] = $arg{pack_limit} // 0;
$d[F_FOLD_LIMIT] = $arg{fold_limit} // 0;
$d[F_JOIN_LIMIT] = $arg{join_limit} // 0;
$d[F_CONTENT_LINES] = 0;
$d[F_ITEMS] = 0;
$d[F_LEAFS] = 0;
$d[F_FOLD_OK] = 1;
$d[F_CHILD_NESTING] = -1;
return bless \@d, $class;
}
# Update Frame information based on added line
sub update_add {
my ($self, $line) = @_ ;
$self->[F_ITEMS] += $line->[L_ITEMS];
$self->[F_LEAFS] += $line->[L_LEAFS];
if ($line->[L_CHILD_NESTING] >= $self->[F_CHILD_NESTING]) {
$self->[F_CHILD_NESTING] = $line->[L_CHILD_NESTING] + 1;
}
}
sub is_empty { return @{ $_[0][F_LINES] } == 0 }
sub last_line { return $_[0][F_LINES][-1] }
# -------------------------------------------------------------------------
# Internal package: counters
# -------------------------------------------------------------------------
package JSON::JSONFold::Stats;
use strict;
use warnings;
sub new {
my ($class) = @_;
return bless {
bytes_in => 0,
bytes_out => 0,
lines_in => 0,
lines_out => 0,
}, $class;
}
sub bytes_in { $_[0]{bytes_in} }
sub bytes_out { $_[0]{bytes_out} }
sub lines_in { $_[0]{lines_in} }
sub lines_out { $_[0]{lines_out} }
sub as_hash { return %{ $_[0] } }
# -------------------------------------------------------------------------
# Internal package: streaming folding filter/writer
# -------------------------------------------------------------------------
package JSON::JSONFold::Writer;
use strict;
use warnings;
BEGIN {
JSON::JSONFold::Line->import() ;
JSON::JSONFold::Frame->import() ;
JSON::JSONFold::Config->import() ;
}
our $SEQ ;
use constant {
W_UNUSED_FIRST => $SEQ++,
W_FH => $SEQ++,
W_CFG => $SEQ++,
W_PENDING => $SEQ++,
W_STACK => $SEQ++,
W_STATS => $SEQ++,
W_DO_CLOSE => $SEQ++,
W_UNUSED_LAST => $SEQ++,
} ;
BEGIN {
our @EXPORT = qw(
W_UNUSED_FIRST
W_FH
W_CFG
W_PENDING
W_STACK
W_STATS
W_DO_CLOSE
W_UNUSED_LAST
) ;
}
sub new {
my ($class, $fh, $config, $do_close) = @_;
my $cfg = JSON::JSONFold::Config::config($config) ;
my @d ;
$#d = $SEQ ;
$d[W_FH] = $fh;
$d[W_CFG] = $cfg unless $cfg->[C_OFF] ;
$d[W_PENDING] = '';
$d[W_STACK] = [];
$d[W_STATS] = JSON::JSONFold::Stats->new;
$d[W_DO_CLOSE] = $do_close;
return bless \@d, $class;
}
sub stats { return $_[0][W_STATS] }
sub write {
my ($self, $s) = @_;
$s = '' unless defined $s;
my $len = length($s);
$self->[W_STATS]{bytes_in} += $len;
lib/JSON/JSONFold.pm view on Meta::CPAN
$self->[W_FH]->close if $self->[W_DO_CLOSE] ;
}
sub _feed {
my ($self, $line) = @_;
# Opener
if ($line->[L_OPENER]) {
push @{ $self->[W_STACK] }, JSON::JSONFold::Frame->new(
kind => $line->[L_OPENER],
depth => scalar(@{ $self->[W_STACK] }),
lines => [ $line ],
pack_limit => $self->_pack_limit($line->[L_OPENER]),
fold_limit => $self->_fold_limit($line->[L_OPENER]),
join_limit => $self->_join_limit($line->[L_OPENER]),
);
return;
}
# Closer
if ($line->[L_CLOSER]) {
$self->_close_frame($line, $line->[L_CLOSER]);
return;
}
# Regular Line
if (@{ $self->[W_STACK] }) {
my $frame = $self->[W_STACK][-1];
$line->[L_CAN_PACK] = 0 if $line->[L_ITEMS] >= $frame->[F_PACK_LIMIT];
$line->[L_CAN_JOIN] = 0 if $line->[L_ITEMS] >= $frame->[F_JOIN_LIMIT];
$self->_add_to_frame($frame, $line);
} else {
$self->_write_line($line);
}
return ;
}
sub _emit_lines {
my ($self, $lines, $depth) = @_;
return unless @$lines;
$depth = @{ $self->[W_STACK] } - 1 unless defined $depth;
if ($depth < 0) {
$self->_write_line($_) for @$lines;
return
}
my $frame = $self->[W_STACK][$depth];
$self->_add_to_frame($frame, $_) for @$lines;
return
}
sub _add_to_frame {
my ($self, $frame, $line) = @_;
if (!$frame->is_empty) {
return if $line->[L_CAN_PACK] && $self->_try_pack($frame, $line);
return if $line->[L_CAN_JOIN] && $self->_try_join($frame, $line);
# If frame is empty, may be it's in "streaming" mode, which
# mean that lines that can not be packed/joined can be sent
# directly to the output:
} elsif (!$frame->[F_FOLD_OK] && !$line->[L_CAN_PACK] && !$line->[L_CAN_JOIN]) {
$self->_write_line($line);
return;
}
push @{ $frame->[F_LINES] }, $line;
if ( $frame->[F_FOLD_OK] && $line->width > $self->[W_CFG][C_WIDTH] ) {
$self->_mark_no_fold ;
}
unless ($line->[L_CLOSER]) {
$frame->[F_CONTENT_LINES]++;
$frame->update_add($line) ;
if ( $frame->[F_FOLD_OK] ) {
$self->_mark_no_fold unless $self->_check_fold_limits($frame)
}
}
$self->_stream_frame($frame) unless $frame->[F_FOLD_OK];
return
}
sub _can_merge {
my ($self, $prev, $line, $limit) = @_;
return $prev->[L_INDENT] == $line->[L_INDENT]
&& $prev->[L_ITEMS] + $line->[L_ITEMS] <= $limit
&& $prev->[L_INDENT] + length($prev->[L_TEXT]) + 1 + length($line->[L_TEXT]) <= $self->[W_CFG][C_WIDTH];
}
sub _merge_into_frame {
my ($self, $frame, $prev, $line) = @_;
$prev->join_line($line);
$frame->update_add($line) ;
$prev->[L_CAN_PACK] = 0 if $prev->[L_ITEMS] >= $frame->[F_PACK_LIMIT];
$prev->[L_CAN_JOIN] = 0 if $prev->[L_ITEMS] >= $frame->[F_JOIN_LIMIT];
if ( $frame->[F_FOLD_OK] ) {
unless ( $self->_check_fold_limits($frame)) {
$self->_mark_no_fold ;
$self->_stream_frame($frame) ;
}
}
}
sub _try_pack {
my ($self, $frame, $line) = @_;
return 0 if $frame->[F_PACK_LIMIT] <= 1 || !$line->[L_CAN_PACK] ||
$frame->is_empty;
my $prev = $frame->last_line;
return 0 unless $prev->[L_CAN_PACK]
&& $prev->[L_CHILD_NESTING] < $self->[W_CFG][C_PACK_NESTING]
&& $self->_can_merge($prev, $line, $frame->[F_PACK_LIMIT]);
$self->_merge_into_frame($frame, $prev, $line);
# Disable join, or pack limits reached
$prev->[L_CAN_JOIN] = 0 unless $prev->[L_CAN_PACK] ;
lib/JSON/JSONFold.pm view on Meta::CPAN
JSON::JSONFold - compact, readable JSON formatting
=head1 SYNOPSIS
use JSON::JSONFold;
# Functional interface
my $text = format_json($data, 100, 'default');
write_json($data, \*STDOUT, 100, 'default');
my $folded = fold_text($pretty_json, 100, 'default');
# Object interface
my $fmt = JSON::JSONFold->new(
width => 100,
config => 'default',
);
my $text = $fmt->format($data);
# JSON-compatible interface
my $text = encode_json($data, {
width => 100,
compact => 'default',
});
# Streaming interface
my $formatter = create_formatter(\*STDOUT, 100, 'default');
$formatter->write($text);
$formatter->finish;
=head1 DESCRIPTION
C<JSON::JSONFold> formats JSON using a regular pretty-printer and then folds
the output into a more compact layout.
It is intended to preserve readability while reducing unnecessary vertical
space in arrays, objects, and simple nested structures.
JSONFold may be used as:
=over
=item *
A functional API.
=item *
An object-oriented formatter.
=item *
A streaming post-processor.
=item *
A drop-in replacement for C<encode_json> and C<to_json>.
=back
=head1 EXPORTED FUNCTIONS
The following functions are exported by default:
format_json
write_json
fold_text
encode_json
to_json
The following functions are exported on request:
jsonfold_config
create_formatter
=head1 FUNCTIONAL INTERFACE
=head2 jsonfold_config
my $config = jsonfold_config($preset, $width, %overrides);
Creates a JSONFold configuration object.
C<$preset> may be a preset name or an existing configuration object.
Additional named arguments override individual configuration settings.
=head2 format_json
my $text = format_json($data, $width, $config, %overrides);
Formats a Perl data structure as folded JSON and returns the resulting text.
=head2 write_json
my $stats = write_json($data, $fh, $width, $config, %overrides);
Formats a Perl data structure and writes the folded JSON to C<$fh>.
Returns formatting statistics.
=head2 fold_text
my $text = fold_text($pretty_json, $width, $config);
Folds existing pretty-printed JSON text and returns the folded result.
=head1 OBJECT INTERFACE
=head2 new
my $fmt = JSON::JSONFold->new(
width => 100,
lib/JSON/JSONFold.pm view on Meta::CPAN
Creates a formatter object.
Recognized options include:
width
Target line width.
config
Preset name or configuration object.
indent
Pretty-print indentation width.
sort_keys
Sort object keys before formatting.
gold
Use JSONFold reference formatting (indent=2, space_before=0, space_after=1). default = true
json
Custom JSON encoder object.
do_close
Close the underlying filehandle when writing finishes.
If C<json> is not supplied, a default pretty-printing JSON encoder is created.
=head2 format
my $text = $fmt->format($data);
Formats a Perl data structure and returns folded JSON text.
=head2 fold
my $text = $fmt->fold($pretty_json);
Folds existing pretty-printed JSON text.
=head2 write
my $stats = $fmt->write($data, $fh);
Formats a Perl data structure and writes the result to C<$fh>.
Returns formatting statistics.
=head2 encode
my $text = $fmt->encode($data);
Alias for C<format>, provided for compatibility with JSON-style APIs.
=head1 STREAMING INTERFACE
=head2 create_formatter
my $formatter = create_formatter($fh, $width, $config, %overrides);
Creates a streaming formatter around an existing filehandle.
The C<$config> parameter may be a preset name or a
L<JSON::JSONFold::Config> object.
The returned object accepts pretty-printed JSON text incrementally and writes
folded JSON to C<$fh>. This allows JSONFold to be used as a streaming
post-processor without buffering the entire document in memory.
my $formatter = create_formatter(\*STDOUT, 100, 'default');
$formatter->write("{\n");
$formatter->write(qq( "name": "Alice"\n));
$formatter->write("}\n");
$formatter->finish;
$formatter->flush;
The returned object is a L<JSON::JSONFold::Writer> and supports:
write($text)
finish()
flush()
close()
stats()
Normally, users should prefer C<format_json>, C<write_json>, or the object
interface. C<create_formatter> is intended for advanced use cases and
integration with existing serializers and streaming APIs.
=head1 JSON-COMPATIBLE FUNCTIONS
=head2 encode_json
This function may be used as a drop-in replacement for C<JSON::encode_json>.
The optional second argument controls JSONFold formatting.
my $text = encode_json($data);
my $text = encode_json($data, {
width => 100,
compact => 'default',
});
Encodes C<$data> as folded JSON.
When called without a second argument, C<encode_json> is compatible with
C<JSON::encode_json> and uses the default JSONFold settings.
The optional second argument is a hash reference containing JSONFold options.
JSONFold-specific options:
=over
=item * C<width>
Target output width.
=item * C<compact>
Preset name or configuration object.
=back
=head2 to_json
This function may be used as a drop-in replacement for C<JSON::to_json>.
The optional second argument controls JSONFold formatting.
It will ignore other legacy C<JSON> options (canonical, etc).
my $text = to_json($data);
my $text = to_json($data, {
canonical => 1,
pretty => 1,
});
my $text = to_json($data, {
width => 100,
compact => 'high',
canonical => 1,
});
Compatibility wrapper similar to C<JSON::to_json>.
When called without a second argument, C<to_json> behaves like
C<JSON::to_json> followed by JSONFold formatting using the default settings.
( run in 1.654 second using v1.01-cache-2.11-cpan-140bd7fdf52 )