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 )