File Coverage

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


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