bcff46aeb47ed8156b8d866f348ffab29a7a0f23
[ikiwiki.git] / IkiWiki / Plugin / sparkline.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::sparkline;
3
4 use warnings;
5 use strict;
6 use IkiWiki 2.00;
7 use IPC::Open2;
8
9 my $match_num=qr/[-+]?[0-9]+(?:\.[0-9]+)?/;
10 my %locmap=(
11         top => 'TEXT_TOP',
12         right => 'TEXT_RIGHT',
13         bottom => 'TEXT_BOTTOM',
14         left => 'TEXT_LEFT',
15 );
16
17 sub import { #{{{
18         hook(type => "preprocess", id => "sparkline", call => \&preprocess);
19 } # }}}
20
21 sub preprocess (@) { #{{{
22         my %params=@_;
23
24         my $php;
25
26         my $style=(exists $params{style} && $params{style} eq "bar") ? "Bar" : "Line";
27         $php=qq{<?php
28                 require_once('sparkline/Sparkline_$style.php');
29                 \$sparkline = new Sparkline_$style();
30                 \$sparkline->SetDebugLevel(DEBUG_NONE);
31         };
32
33         foreach my $param (qw{BarWidth BarSpacing YMin YMaz}) {
34                 if (exists $params{lc($param)}) {
35                         $php.=qq{\$sparkline->Set$param(}.int($params{lc($param)}).qq{);\n};
36                 }
37         }
38
39         my $c=0;
40         while (@_) {
41                 my $key=shift;
42                 my $value=shift;
43
44                 if ($key=~/^($match_num)(?:,($match_num))?(?:\(([a-z]+)\))?$/) {
45                         $c++;
46                         my ($x, $y);
47                         if (defined $2) {
48                                 $x=$1;
49                                 $y=$2;
50                         }
51                         else {
52                                 $x=$c;
53                                 $y=$1;
54                         }
55                         if ($style eq "Bar" && defined $3) {
56                                 $php.=qq{\$sparkline->SetData($x, $y, '$3');\n};
57                         }
58                         else {
59                                 $php.=qq{\$sparkline->SetData($x, $y);\n};
60                         }
61                 }
62                 elsif (! length $value) {
63                         error gettext("parse error")." \"$key\"";
64                 }
65                 elsif ($key eq 'featurepoint') {
66                         my ($x, $y, $color, $diameter, $text, $location)=
67                                 split(/\s*,\s*/, $value);
68                         if (! defined $diameter || $diameter < 0) {
69                                 error gettext("bad featurepoint diameter");
70                         }
71                         $x=int($x);
72                         $y=int($y);
73                         $color=~s/[^a-z]+//g;
74                         $diameter=int($diameter);
75                         $text=~s/[^-a-zA-Z0-9]+//g if defined $text;
76                         if (defined $location) {
77                                 $location=$locmap{$location};
78                                 if (! defined $location) {
79                                         error gettext("bad featurepoint location");
80                                 }
81                         }
82                         $php.=qq{\$sparkline->SetFeaturePoint($x, $y, '$color', $diameter};
83                         $php.=qq{, '$text'} if defined $text;
84                         $php.=qq{, $location} if defined $location;
85                         $php.=qq{);\n};
86                 }
87         }
88
89         if ($c eq 0) {
90                 error gettext("missing values");
91         }
92
93         my $height=int($params{height} || 20);
94         if ($height < 2 || $height > 100) {
95                 error gettext("bad height value");
96         }
97         if ($style eq "Bar") {
98                 $php.=qq{\$sparkline->Render($height);\n};
99         }
100         else {
101                 if (! exists $params{width}) {
102                         error gettext("missing width parameter");
103                 }
104                 my $width=int($params{width});
105                 if ($width < 2 || $width > 1024) {
106                         error gettext("bad width value");
107                 }
108                 $php.=qq{\$sparkline->RenderResampled($width, $height);\n};
109         }
110         
111         $php.=qq{\$sparkline->Output();\n?>\n};
112
113         # Use the sha1 of the php code that generates the sparkline as
114         # the base for its filename.
115         eval q{use Digest::SHA1};
116         error($@) if $@;
117         my $fn=$params{page}."/sparkline-".
118                 IkiWiki::possibly_foolish_untaint(Digest::SHA1::sha1_hex($php)).
119                 ".png";
120         will_render($params{page}, $fn);
121
122         if (! -e "$config{destdir}/$fn") {
123                 my $pid;
124                 my $sigpipe=0;;
125                 $SIG{PIPE}=sub { $sigpipe=1 };
126                 $pid=open2(*IN, *OUT, "php");
127
128                 # open2 doesn't respect "use open ':utf8'"
129                 binmode (OUT, ':utf8');
130
131                 print OUT $php;
132                 close OUT;
133
134                 my $png;
135                 {
136                         local $/=undef;
137                         $png=<IN>;
138                 }
139                 close IN;
140
141                 waitpid $pid, 0;
142                 $SIG{PIPE}="DEFAULT";
143                 if ($sigpipe) {
144                         error gettext("failed to run php");
145                 }
146
147                 if (! $params{preview}) {
148                         writefile($fn, $config{destdir}, $png, 1);
149                 }
150                 else {
151                         # can't write the file, so embed it in a data uri
152                         eval q{use MIME::Base64};
153                         error($@) if $@;
154                         return "<img src=\"data:image/png;base64,".
155                                 encode_base64($png)."\" />";
156                 }
157         }
158
159         return '<img src="'.urlto($fn, $params{destpage}).'" alt="graph" />';
160 } # }}}
161
162 1