JSON-Structure

 view release on metacpan or  search on metacpan

lib/JSON/Structure/JsonSourceLocator.pm  view on Meta::CPAN

sub new {
    my ( $class, $json_text ) = @_;

    my $self = bless {
        json_text    => $json_text // '',
        line_offsets => [],
    }, $class;

    $self->_build_line_offsets();

    return $self;
}

=head2 get_location($path)

Returns a JsonLocation object for the given JSON Pointer path.

    my $location = $locator->get_location('#/properties/name');

=cut

sub get_location {
    my ( $self, $path ) = @_;

    return JSON::Structure::Types::JsonLocation->unknown()
      unless defined $path && length( $self->{json_text} );

    # Parse the JSON Pointer path into segments
    my @segments = $self->_parse_json_pointer($path);

    # Find the location in the text
    return $self->_find_location_in_text( \@segments );
}

sub _build_line_offsets {
    my ($self) = @_;

    my @offsets = (0);                  # First line starts at offset 0
    my $text    = $self->{json_text};

    for ( my $i = 0 ; $i < length($text) ; $i++ ) {
        if ( substr( $text, $i, 1 ) eq "\n" ) {
            push @offsets, $i + 1;
        }
    }

    $self->{line_offsets} = \@offsets;
}

sub _parse_json_pointer {
    my ( $self, $path ) = @_;

    # Remove leading # if present (JSON Pointer fragment identifier)
    $path =~ s/^#//;

    # Handle empty path or just "/"
    return () if !defined $path || $path eq '' || $path eq '/';

    my @segments;

    for my $segment ( split m{/}, $path ) {
        next if $segment eq '';

        # Unescape JSON Pointer tokens
        $segment =~ s/~1/\//g;
        $segment =~ s/~0/~/g;

        # Handle bracket notation (e.g., "required[0]" -> "required", "0")
        if ( $segment =~ /^([^\[]+)\[(.+)\]$/ ) {
            push @segments, $1;
            my $rest = "[$2]";

            while ( $rest =~ /^\[([^\]]+)\](.*)$/ ) {
                push @segments, $1;
                $rest = $2;
            }
        }
        else {
            push @segments, $segment;
        }
    }

    return @segments;
}

sub _offset_to_location {
    my ( $self, $offset ) = @_;

    return JSON::Structure::Types::JsonLocation->unknown()
      if $offset < 0 || $offset > length( $self->{json_text} );

    my $offsets = $self->{line_offsets};

    # Binary search for the line
    my ( $low, $high ) = ( 0, $#$offsets );

    while ( $low < $high ) {
        my $mid = int( ( $low + $high + 1 ) / 2 );
        if ( $offsets->[$mid] <= $offset ) {
            $low = $mid;
        }
        else {
            $high = $mid - 1;
        }
    }

    my $line   = $low + 1;                          # 1-based line number
    my $column = $offset - $offsets->[$low] + 1;    # 1-based column number

    return JSON::Structure::Types::JsonLocation->new(
        line   => $line,
        column => $column,
    );
}

sub _find_location_in_text {
    my ( $self, $segments ) = @_;

    my $text = $self->{json_text};
    my $pos  = 0;



( run in 0.619 second using v1.01-cache-2.11-cpan-71847e10f99 )