File Coverage

blib/lib/Mac/Safari/JavaScript.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Mac::Safari::JavaScript;
2 3     3   70570 use base qw(Exporter);
  3         6  
  3         351  
3              
4             # This isn't a problem, all Macs have at least 5.8
5 3     3   66 use 5.008;
  3         9  
  3         106  
6              
7 3     3   16 use strict;
  3         18  
  3         111  
8 3     3   15 use warnings;
  3         5  
  3         97  
9              
10 3     3   3961 use Mac::AppleScript qw(RunAppleScript);
  0            
  0            
11             use JSON::XS;
12             use Encode qw(encode decode);
13             use Carp qw(croak);
14              
15             use Mac::Safari::JavaScript::Exception;
16              
17             our @EXPORT_OK;
18             our $VERSION = "1.04";
19              
20             =head1 NAME
21              
22             Mac::Safari::JavaScript - Run JavaScript in Safari on Mac OS X
23              
24             =head1 SYNOPSIS
25              
26             use Mac::Safari::JavaScript qw(safari_js);
27              
28             # do an alert
29             safari_js 'alert("Hello Safari User")';
30              
31             # return some value
32             var $arrayref = safari_js 'return [1,2,3]';
33              
34             # multiple lines are okay
35             safari_js <<'JAVASCRIPT';
36             var fred = "bob";
37             return fred;
38             JAVASCRIPT
39            
40             # You can set variables to pass in
41             safari_js 'return document.getElementById(id).href', id => "mainlink";
42              
43             =head1 DESCRIPTION
44              
45             This module allows you to execute JavaScript code in the Safari web
46             browser on Mac OS X.
47              
48             The current implementation wraps the JavaScript in Applescript,
49             compiles it, and executes it in order to control Safari.
50              
51             =head1 FUNCTIONS
52              
53             Functions are exported on request, or may be called fully qualified.
54              
55             =over
56              
57             =item safari_js($javascript, @named_parameters)
58              
59             Runs the JavaScript in the first tab of the front window of the
60             currently running Safari.
61              
62             =over 8
63              
64             =item The script
65              
66             This script may safely contain newlines, unicode characters, comments etc.
67             Any line numbers in error messages should match up with error messages
68              
69             =item Return value
70              
71             C will do a passable job of mapping whatever you returned from
72             your JavaScript (using the C keyword) into a Perl data structure it
73             will return. If you do not return a value from JavaScript (i.e. the return
74             keyword is not executed) then C will return the empty list. If
75             you return nothing (i.e. use C in your script), C will
76             return C.
77              
78             Whatever you return from your JavaScript will be encoded into JSON with
79             Safari's native C function and decoded on the Perl side
80             using the JSON::XS module.
81              
82             JavaScript data structures are mapped as you might expect: Objects to
83             hashrefs, Arrays to arrayrefs, strings and numbers to their normal scalar
84             representation, and C, C and C to C, JSON::XS::true
85             (which you can treat like the scalar C<1>) and JSON::XS::false (which you
86             can treat like the scalar C<0>) respectivly. Please see L
87             for more information.
88              
89             You cannot return anything from JavaScript that has a ciruclar reference
90             in it (as this cannot be represented by JSON.)
91              
92             =item Passing Parameters
93              
94             You may pass in named parameters by passing them as name/value pairs
95              
96             safari_js $js_code_to_run, name1 => $value1, name2 => $value2, ...
97              
98             The parameters are simply availble as variables in your code.
99              
100             Internally parameters are converted from Perl data structures into JavaScript
101             using JSON::XS using the reverse mapping described above. You may not pass
102             in circular data structures. Again, see L for more infomation.
103              
104             =item Exception Handling
105              
106             If what you pass causes an uncaught exception within the Safari web browser
107             (including exceptions during by parsing your script) then a
108             Mac::Safari::JavaScript::Exception exception object will be raised by
109             C. This will stringify to the exception you normally would see
110             in your browser and can be integated for extra info such as the line number,
111             etc.
112              
113             =back
114              
115             =cut
116              
117             sub safari_js($;@) {
118             my $javascript = shift;
119              
120             # create a coder objects
121             my $coder = JSON::XS->new;
122             $coder->allow_nonref(1);
123              
124             # handle the arguments passed in
125             if (@_ % 2) {
126             croak "Uneven number of parameters passed to safari_js";
127             }
128             my %params;
129             while (@_) {
130             my $key = shift;
131             if (exists $params{ $key }) {
132             croak "Duplicate parameter '$key' passed twice to safari_js";
133             }
134             # we're going to put the value into a string to
135             # eval. This means we need to escape all the meta chars
136             my $value = $coder->encode(shift);
137             $value =~ s/\\/\\\\/gx; # \ -> \\
138             $value =~ s/"/\\"/gx; # " -> \"
139              
140             $params{ $key } = $value;
141             }
142             my $args = join ",", keys %params;
143             my $values = join ",", values %params;
144              
145             # we're going to put the javascript into a string to
146             # eval. This means we need to escape all the meta chars
147             $javascript =~ s/\\/\\\\/gx; # \ -> \\
148             $javascript =~ s/"/\\"/gx; # " -> \"
149              
150             # since we've now effectivly got a multiline string (and
151             # JavaScript doesn't support that) we better fix that up
152             # Note that we're trying not to mess up the line numers
153             # inside the eval of what we were passed
154             $javascript = join '\\n"+"',split /\n/x, $javascript;
155              
156             # wrap the javascript in helper functions
157             #
158             # - use (function () { })() to avoid poluting the global namespace
159             # - use eval "" to allow syntax errors to be caught and returned as a
160             # data structure we can re-throw on the Perl side
161             # - JSON.stringify(undefined) returns, not the string "null". We detect
162             # this and return the string "null"
163              
164             $javascript = <<"ENDOFJAVASCRIPT";
165             try{var result=eval("JSON.stringify((function($args){ $javascript;throw'NothingReturned'})($values));");(result===undefined)?'{"undefined":1}':'{"result":'+result+'}';}catch(e){ (e == "NothingReturned")?'{"noresult":1}':(function(){var r={error:e,name:'CustomError'};var v=['name',"line","expressionBeginOffset","expressionEndOffset","message","sourceId","sourceURL"];console.log(e);for(var i=0;i
166             ENDOFJAVASCRIPT
167              
168             # escape the string escapes again as we're going to pass
169             # the whole thing via Applescript now
170             $javascript =~ s/\\/\\\\/gx; # \ -> \\
171             $javascript =~ s/"/\\"/gx; # " -> \"
172              
173             # wrap it in applescript
174             my $applescript = <<"ENDOFAPPLESCRIPT";
175             tell application "Safari"
176             -- execute the javascript
177             set result to do JavaScript "$javascript" in document 1
178              
179             -- then make sure we're returning a string to be consistent'
180             "" & result
181             end tell
182             ENDOFAPPLESCRIPT
183              
184             # compile it an execute it using the cocca api
185             # (make sure to pass it in as utf-8 bytes)
186             my $json = RunAppleScript($applescript);
187              
188             # $json is now a string where each character represents a byte
189             # in a utf-8 encoding of the real characters (ARGH!). Fix that so
190             # each character actually represents the character it should, um,
191             # represent.
192             $json = encode("iso-8859-1", $json);
193             $json = decode("utf-8", $json);
194              
195             # strip off any applescript string wrapper
196             $json =~ s/\A"//x;
197             $json =~ s/"\z//x;
198             $json =~ s/\\"/"/gx;
199             $json =~ s/\\\\/\\/gx;
200              
201             # and decode this from json
202             my $ds = eval {
203             $coder->decode($json);
204             };
205             if ($@) { croak("Unexpected error returned when trying to communicate with Safari"); }
206              
207             return undef
208             if exists $ds->{undefined};
209             return
210             if exists $ds->{noresult};
211             return $ds->{result}
212             if exists $ds->{result};
213             croak(Mac::Safari::JavaScript::Exception->new(%{ $ds }))
214             if exists $ds->{error};
215             croak("Unexpected error returned when trying to communicate with Safari");
216             }
217             push @EXPORT_OK, "safari_js";
218              
219             =back
220              
221             =head1 AUTHOR
222              
223             Written by Mark Fowler
224              
225             Copryright Mark Fowler 2011. All Rights Reserved.
226              
227             This program is free software; you can redistribute it
228             and/or modify it under the same terms as Perl itself.
229              
230             =head1 BUGS
231              
232             Bugs should be reported to me via the CPAN RT system. http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Mac::Safari::JavaScript
233              
234             Some pages (e.g. http://developer.apple.com/) cause array stringifcation to break. I haven't worked out why yet.
235              
236             =head1 SEE ALSO
237              
238             L, L
239              
240             =cut
241              
242             1;