File Coverage

blib/lib/JavaScript/HashRef/Decode.pm
Criterion Covered Total %
statement 37 37 100.0
branch 8 8 100.0
condition n/a
subroutine 15 15 100.0
pod 1 1 100.0
total 61 61 100.0


line stmt bran cond sub pod time code
1             package JavaScript::HashRef::Decode;
2             $JavaScript::HashRef::Decode::VERSION = '0.221231';
3             ## ABSTRACT: JavaScript "simple object" (hashref) decoder
4              
5 3     3   108625 use v5.10;
  3         24  
6 3     3   13 use strict;
  3         5  
  3         62  
7 3     3   15 use warnings;
  3         4  
  3         90  
8 3     3   2697 use Parse::RecDescent;
  3         98582  
  3         20  
9 3     3   122 use Exporter qw;
  3         5  
  3         1679  
10             our @EXPORT_OK = qw;
11              
12             our $js_grammar = <<'END_GRAMMAR';
13             number: hex | decimal
14             hex: / 0 [xX] [0-9a-fA-F]+ (?! \w ) /x
15             {
16             $return = bless {
17             value => hex lc $item[1],
18             }, 'JavaScript::HashRef::Decode::NUMBER';
19             }
20             decimal: /(?: (?: 0 | -? [1-9][0-9]* ) \. [0-9]*
21             | \. [0-9]+
22             | (?: 0 | -? [1-9][0-9]* ) )
23             (?: [eE][-+]?[0-9]+ )?
24             (?! \w )/x
25             {
26             $return = bless {
27             value => 0+$item[1],
28             }, 'JavaScript::HashRef::Decode::NUMBER';
29             }
30             string_double_quoted:
31             m{" # Starts with a single-quote
32             ( # Start capturing *inside* the double-quote
33             [^\\"\r\n\x{2028}\x{2029}]*+ # Any ordinary characters
34             (?: # Followed by ...
35             \\ [^\r\n\x{2028}\x{2029}] # Any backslash sequence
36             [^\\"\r\n\x{2028}\x{2029}]*+ # ... followed by any ordinary characters
37             )*+ # 0+ times
38             ) # End capturing *inside* the double-quote
39             " # Ends with a double-quote
40             }x
41             {
42             $return = bless {
43             value => "$1",
44             }, 'JavaScript::HashRef::Decode::STRING';
45             }
46             string_single_quoted:
47             m{' # Starts with a single-quote
48             ( # Start capturing *inside* the single-quote
49             [^\\'\r\n\x{2028}\x{2029}]*+ # Any ordinary characters
50             (?: # Followed by ...
51             \\ [^\r\n\x{2028}\x{2029}] # Any backslash sequence
52             [^\\'\r\n\x{2028}\x{2029}]*+ # ... followed by any ordinary characters
53             )*+ # 0+ times
54             ) # End capturing *inside* the single-quote
55             ' # Ends with a single-quote
56             }x
57             {
58             $return = bless {
59             value => "$1",
60             }, 'JavaScript::HashRef::Decode::STRING';
61             }
62             unescaped_key: m{[a-zA-Z_0-9]+}
63             {
64             $return = bless {
65             key => $item[1],
66             }, 'JavaScript::HashRef::Decode::KEY';
67             }
68             string: string_single_quoted | string_double_quoted
69             key: unescaped_key | string
70             token_undefined: "undefined"
71             {
72             $return = bless {
73             }, 'JavaScript::HashRef::Decode::UNDEFINED';
74             }
75             token_null: "null"
76             {
77             $return = bless {
78             }, 'JavaScript::HashRef::Decode::UNDEFINED';
79             }
80             undefined: token_undefined | token_null
81             true: "true"
82             {
83             $return = bless {
84             }, 'JavaScript::HashRef::Decode::TRUE';
85             }
86             false: "false"
87             {
88             $return = bless {
89             }, 'JavaScript::HashRef::Decode::FALSE';
90             }
91             boolean: true | false
92             any_value: number | string | hashref | arrayref | undefined | boolean
93             tuple: key ":" any_value
94             {
95             $return = bless {
96             key => $item[1],
97             value => $item[3],
98             }, 'JavaScript::HashRef::Decode::TUPLE';
99             }
100             list_of_values: (s?)
101             arrayref: "[" list_of_values "]"
102             {
103             $return = bless $item[2], 'JavaScript::HashRef::Decode::ARRAYREF';
104             }
105             tuples: (s?)
106             hashref: "{" tuples "}"
107             {
108             $return = bless $item[2], 'JavaScript::HashRef::Decode::HASHREF';
109             }
110             END_GRAMMAR
111              
112             our $parser;
113              
114             =head1 NAME
115              
116             JavaScript::HashRef::Decode - a JavaScript "data hashref" decoder for Perl
117              
118             =head1 DESCRIPTION
119              
120             This module "decodes" a simple data-only JavaScript "variable value" and
121             returns a Perl data structure constructed from the data contained in it.
122              
123             It only supports "data" which comprises of: hashrefs, arrayrefs, single- and
124             double-quoted strings, numbers, and "special" token the likes of "undefined",
125             "true", "false", "null".
126              
127             It does not support functions, nor is it meant to be an all-encompassing parser
128             for a JavaScript object.
129              
130             If you feel like the JavaScript structure you'd like to parse cannot
131             effectively be parsed by this module, feel free to look into the
132             L grammar of this module.
133              
134             Patches are always welcome.
135              
136             =head1 SYNOPSIS
137              
138             use JavaScript::HashRef::Decode qw;
139             use Data::Dumper::Concise;
140             my $js = q!{ foo: "bar", baz: { quux: 123 } }!;
141             my $href = decode_js($js);
142             print Dumper $href;
143             {
144             baz => {
145             quux => 123
146             },
147             foo => "bar"
148             }
149              
150             =head1 EXPORTED SUBROUTINES
151              
152             =head2 C
153              
154             Given a JavaScript "variable value" thing (i.e. an hashref or arrayref or
155             string, number, etc), returns a Perl hashref structure which corresponds to the
156             given data
157              
158             decode_js('{foo:"bar"}');
159              
160             Returns a Perl hashref:
161              
162             { foo => 'bar' }
163              
164             The L internal interface is reused across invocations.
165              
166             =cut
167              
168             sub decode_js {
169 10     10 1 4951 my ($str) = @_;
170              
171 10 100       47 $parser = Parse::RecDescent->new($js_grammar)
172             if !defined $parser;
173 10         132834 my $parsed = $parser->any_value($str);
174 10 100       44644 die "decode_js: Cannot parse (invalid js?) \"$str\""
175             if !defined $parsed;
176 9         58 return $parsed->out;
177             }
178              
179             # For each "type", provide an ->out function which returns the proper Perl type
180             # for the structure, possibly recursively
181              
182             package JavaScript::HashRef::Decode::NUMBER;
183             $JavaScript::HashRef::Decode::NUMBER::VERSION = '0.221231';
184             sub out {
185 32     32   9931 return $_[0]->{value};
186             }
187              
188             package JavaScript::HashRef::Decode::STRING;
189             $JavaScript::HashRef::Decode::STRING::VERSION = '0.221231';
190             my %unescape = (
191             'b' => "\b",
192             'f' => "\f",
193             'n' => "\n",
194             'r' => "\r",
195             't' => "\t",
196             'v' => "\x0B",
197             '"' => '"',
198             "'" => "'",
199             '0' => "\0",
200             '\\' => '\\',
201             );
202              
203             my $unescape_rx = do {
204             my $fixed = join '|', map quotemeta, grep $_, reverse sort keys %unescape;
205             qr/\\($fixed|0(?![0-9])|(x([0-9a-fA-F]{2})|u([0-9a-fA-F]{4}))|.)/;
206             };
207              
208             sub out {
209 23     23   15498 my $val = $_[0]->{value};
210 23         44 $val =~ s{\\u[dD]([89abAB][0-9a-fA-F][0-9a-fA-F])
211 1         7 \\u[dD]([c-fC-F][0-9a-fA-F][0-9a-fA-F])}
212 23 100       103 { chr(0x10000 + ((hex($1) - 0x800 << 10) | hex($2) - 0xC00)) }xmsge;
  16 100       75  
213 23         69 $val =~ s/$unescape_rx/$unescape{$1} || ($2 ? chr(hex $+) : $1)/ge;
214             return $val;
215             }
216              
217             package JavaScript::HashRef::Decode::UNDEFINED;
218             $JavaScript::HashRef::Decode::UNDEFINED::VERSION = '0.221231';
219 6     6   59333 sub out {
220             return undef;
221             }
222              
223             package JavaScript::HashRef::Decode::TRUE;
224             $JavaScript::HashRef::Decode::TRUE::VERSION = '0.221231';
225 1     1   873 sub out {
226             return ( 1 == 1 );
227             }
228              
229             package JavaScript::HashRef::Decode::FALSE;
230             $JavaScript::HashRef::Decode::FALSE::VERSION = '0.221231';
231 2     2   1023 sub out {
232             return ( 1 == 0 );
233             }
234              
235             package JavaScript::HashRef::Decode::ARRAYREF;
236             $JavaScript::HashRef::Decode::ARRAYREF::VERSION = '0.221231';
237 9     9   24849 sub out {
  24         58  
  9         39  
238             return [ map { $_->out } @{ $_[0] } ];
239             }
240              
241             package JavaScript::HashRef::Decode::KEY;
242             $JavaScript::HashRef::Decode::KEY::VERSION = '0.221231';
243 20     20   1219 sub out {
244             return $_[0]->{key};
245             }
246              
247             package JavaScript::HashRef::Decode::TUPLE;
248             $JavaScript::HashRef::Decode::TUPLE::VERSION = '0.221231';
249 20     20   48 sub out {
250             return $_[0]->{key}->out => $_[0]->{value}->out;
251             }
252              
253             package JavaScript::HashRef::Decode::HASHREF;
254             $JavaScript::HashRef::Decode::HASHREF::VERSION = '0.221231';
255 13     13   17984 sub out {
  20         47  
  13         51  
256             return { map { $_->out } @{ $_[0] } };
257             }
258              
259             =head1 SEE ALSO
260              
261             L
262              
263             The ECMAScript Object specification: L
264              
265             =head1 AUTHOR
266              
267             Marco Fontani - L
268              
269             =head1 CONTRIBUTORS
270              
271             Aaron Crane
272              
273             =head1 COPYRIGHT
274              
275             Copyright (c) 2013 Situation Publishing LTD
276              
277             =cut
278              
279             1;