aboutsummaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/graphviz.pm
blob: 479da6f7382c41e664297a3ef22e665fc4cd5812 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#!/usr/bin/perl
# graphviz plugin for ikiwiki: render graphviz source as an image.
# Josh Triplett
package IkiWiki::Plugin::graphviz;

use warnings;
use strict;
use IkiWiki 3.00;
use IPC::Open2;

sub import {
	hook(type => "getsetup", id => "graphviz", call => \&getsetup);
	hook(type => "needsbuild", id => "version", call => \&needsbuild);
	hook(type => "preprocess", id => "graph", call => \&graph, scan => 1);
}

sub getsetup () {
	return
		plugin => {
			safe => 1,
			rebuild => undef,
			section => "widget",
		},
}

my %graphviz_programs = (
	"dot" => 1, "neato" => 1, "fdp" => 1, "twopi" => 1, "circo" => 1
);

sub needsbuild {
	my $needsbuild=shift;
	foreach my $page (keys %pagestate) {
		if (exists $pagestate{$page}{graph} &&
		    exists $pagesources{$page} &&
		    grep { $_ eq $pagesources{$page} } @$needsbuild) {
			# remove state, will be re-added if
			# the graph is still there during the rebuild
			delete $pagestate{$page}{graph};
		}
	}       
	return $needsbuild;
}

sub render_graph (\%) {
	my %params = %{(shift)};
	
	my $src = "charset=\"utf-8\";\n";
	$src .= "ratio=compress;\nsize=\"".($params{width}+0).", ".($params{height}+0)."\";\n"
		if defined $params{width} and defined $params{height};
	$src .= $params{src};
	$src .= "}\n";
	
	# Use the sha1 of the graphviz code as part of its filename,
	# and as a unique identifier for its imagemap.
	eval q{use Digest::SHA};
	error($@) if $@;
	my $sha=IkiWiki::possibly_foolish_untaint(Digest::SHA::sha1_hex($params{type}.$src));
	$src = "$params{type} graph$sha {\n".$src;

	my $dest=$params{page}."/graph-".$sha.".png";
	will_render($params{page}, $dest);

	my $map=$pagestate{$params{destpage}}{graph}{$sha};
	if (! -e "$config{destdir}/$dest" || ! defined $map) {
		# Use ikiwiki's function to create the image file, this makes
		# sure needed subdirs are there and does some sanity checking.
		writefile($dest, $config{destdir}, "");
		
		my $pid;
		my $sigpipe=0;
		$SIG{PIPE}=sub { $sigpipe=1 };
		$pid=open2(*IN, *OUT, "$params{prog} -Tpng -o '$config{destdir}/$dest' -Tcmapx");

		# open2 doesn't respect "use open ':utf8'"
		binmode (IN, ':utf8');
		binmode (OUT, ':utf8');

		print OUT $src;
		close OUT;

		local $/ = undef;
		$map=$pagestate{$params{destpage}}{graph}{$sha}=<IN>;
		close IN;

		waitpid $pid, 0;
		$SIG{PIPE}="DEFAULT";
		error gettext("failed to run graphviz") if ($sigpipe || $?);
	}

	return "<img src=\"".urlto($dest, $params{destpage}).
		"\" usemap=\"#graph$sha\" />\n".
		$map;
}

sub graph (@) {
	my %params=@_;

	if (exists $params{file}) {
		if (! exists $pagesources{$params{file}}) {
			error gettext("cannot find file");
		}
		$params{src} = readfile(srcfile($params{file}));
		add_depends($params{page}, $params{file});
	}

	# Support wikilinks in the graph source.
	my $src=$params{src};
	$src="" unless defined $src;
	$src=IkiWiki::linkify($params{page}, $params{destpage}, $params{src});
	return unless defined wantarray; # scan mode short-circuit
	if ($src ne $params{src}) {
		# linkify makes html links, but graphviz wants plain
		# urls. This is, frankly a hack: Process source as html,
		# throw out everything inside tags that is not a href.
		my $s;
		my $nested=0;
		use HTML::Parser;
		error $@ if $@;
		my $p=HTML::Parser->new(api_version => 3);
		$p->handler(start => sub {
			my %attrs=%{shift()};
			if (exists $attrs{href}) {
				if ($s=~/href\s*=\s*"$/) {
					$s.=$attrs{href};
				}
				elsif ($s=~/href\s*=\s*$/) {
					$s.="\"$attrs{href}\"";
				}
				else {
					$s.="href=\"$attrs{href}\"";
				}
			}
			$nested++;
		}, "attr");
		$p->handler(end => sub {
			$nested--;
		});
		$p->handler(default => sub {
			$s.=join("", @_) unless $nested;
		}, "text");
		$p->parse($src);
		$p->eof;
		$s=~s/\[ href= \]//g; # handle self-links
		$params{src}=$s;
	}
	else {
		$params{src}=$src;
	}

	$params{type} = "digraph" unless defined $params{type};
	$params{prog} = "dot" unless defined $params{prog};
	error gettext("prog not a valid graphviz program") unless $graphviz_programs{$params{prog}};

	return render_graph(%params);
}

1