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