1#!/usr/bin/perl
2package IkiWiki::Plugin::highlight;
3
4# This has been tested with highlight 2.16 and highlight 3.2+svn19.
5# In particular version 3.2 won't work. It detects the different
6# versions by the presence of the the highlight::DataDir class.
7
8use warnings;
9use strict;
10use IkiWiki 3.00;
11use Encode;
12
13my $data_dir;
14
15sub import {
16	hook(type => "getsetup", id => "highlight",  call => \&getsetup);
17	hook(type => "checkconfig", id => "highlight", call => \&checkconfig);
18	# this hook is used by the format plugin
19	hook(type => "htmlizeformat", id => "highlight",
20		call => \&htmlizeformat, last => 1);
21}
22
23sub getsetup () {
24	return
25		plugin => {
26			safe => 1,
27			rebuild => 1, # format plugin
28			section => "format",
29		},
30		tohighlight => {
31			type => "string",
32			example => ".c .h .cpp .pl .py Makefile:make",
33			description => "types of source files to syntax highlight",
34			safe => 1,
35			rebuild => 1,
36		},
37		filetypes_conf => {
38			type => "string",
39			example => "/usr/local/etc/highlight/filetypes.conf",
40			description => "location of highlight's filetypes.conf",
41			safe => 0,
42			rebuild => undef,
43		},
44		langdefdir => {
45			type => "string",
46			example => "/usr/local/share/highlight/langDefs",
47			description => "location of highlight's langDefs directory",
48			safe => 0,
49			rebuild => undef,
50		},
51}
52
53sub checkconfig () {
54	eval q{use highlight};
55	if (highlight::DataDir->can('new')) {
56		$data_dir=new highlight::DataDir();
57		$data_dir->searchDataDir("");
58	} else {
59		$data_dir=undef;
60	}
61
62	if (! exists $config{filetypes_conf}) {
63	  if (! $data_dir ) {
64		$config{filetypes_conf}= "/usr/local/etc/highlight/filetypes.conf";
65	      } elsif ( $data_dir -> can('getFiletypesConfPath') ) {
66		# 3.14 +
67		$config{filetypes_conf}=
68		  $data_dir -> getFiletypesConfPath("filetypes");
69	      } else {
70		# 3.9 +
71		$config{filetypes_conf}=
72		  $data_dir -> getConfDir() . "/filetypes.conf";
73	      }
74	}
75	# note that this is only used for old versions of highlight
76	# where $data_dir will not be defined.
77	if (! exists $config{langdefdir}) {
78		$config{langdefdir}= "/usr/local/share/highlight/langDefs";
79
80	}
81	if (exists $config{tohighlight} && read_filetypes()) {
82		foreach my $file (split ' ', $config{tohighlight}) {
83			my @opts = $file=~s/^\.// ?
84				(keepextension => 1) :
85				(noextension => 1);
86			my $ext = $file=~s/:(.*)// ? $1 : $file;
87
88			my $langfile=ext2langfile($ext);
89			if (! defined $langfile) {
90				error(sprintf(gettext(
91					"tohighlight contains unknown file type '%s'"),
92					$ext));
93			}
94
95			hook(
96				type => "htmlize",
97				id => $file,
98				call => sub {
99					my %params=@_;
100				       	highlight($langfile, $file, $params{content});
101				},
102				longname => sprintf(gettext("Source code: %s"), $file),
103				@opts,
104			);
105		}
106	}
107}
108
109sub htmlizeformat {
110	my $format=lc shift;
111	my $langfile=ext2langfile($format);
112
113	if (! defined $langfile) {
114		return;
115	}
116
117	return Encode::decode_utf8(highlight($langfile, $format, shift));
118}
119
120my %ext2lang;
121my $filetypes_read=0;
122my %highlighters;
123
124# Parse highlight's config file to get extension => language mappings.
125sub read_filetypes () {
126	my $f;
127	if (!open($f, $config{filetypes_conf})) {
128		warn($config{filetypes_conf}.": ".$!);
129		return 0;
130	};
131
132	local $/=undef;
133	my $config=<$f>;
134	close $f;
135
136	# highlight >= 3.2 format (bind-style)
137	while ($config=~m/Lang\s*=\s*\"([^"]+)\"[,\s]+Extensions\s*=\s*{([^}]+)}/sg) {
138		my $lang=$1;
139		foreach my $bit (split ',', $2) {
140			$bit=~s/.*"(.*)".*/$1/s;
141			$ext2lang{$bit}=$lang;
142		}
143	}
144
145	# highlight < 3.2 format
146	if (! keys %ext2lang) {
147		foreach (split("\n", $config)) {
148			if (/^\$ext\((.*)\)=(.*)$/) {
149				$ext2lang{$_}=$1 foreach $1, split ' ', $2;
150			}
151		}
152	}
153
154	return $filetypes_read=1;
155}
156
157
158sub searchlangdef {
159  my $lang=shift;
160
161  if ($data_dir) {
162    return $data_dir->getLangPath($lang . ".lang");
163  } else {
164    return "$config{langdefdir}/$lang.lang";
165  }
166
167}
168# Given a filename extension, determines the language definition to
169# use to highlight it.
170sub ext2langfile ($) {
171	my $ext=shift;
172
173	my $langfile=searchlangdef($ext);
174	return $langfile if exists $highlighters{$langfile};
175
176	read_filetypes() unless $filetypes_read;
177	if (exists $ext2lang{$ext}) {
178		return searchlangdef($ext2lang{$ext});
179	}
180	# If a language only has one common extension, it will not
181	# be listed in filetypes, so check the langfile.
182	elsif (-e $langfile) {
183		return $langfile;
184	}
185	else {
186		return undef;
187	}
188}
189
190# Interface to the highlight C library.
191sub highlight ($$) {
192	my $langfile=shift;
193	my $extorfile=shift;
194	my $input=shift;
195
196	eval q{use highlight};
197	if ($@) {
198		print STDERR gettext("warning: highlight perl module not available; falling back to pass through");
199		return $input;
200	}
201
202	my $gen;
203	if (! exists $highlighters{$langfile}) {
204		no warnings 'once';
205		$gen = highlight::CodeGenerator::getInstance($highlight::XHTML);
206		use warnings;
207		$gen->setFragmentCode(1); # generate html fragment
208		$gen->setHTMLEnclosePreTag(1); # include stylish <pre>
209		if ($data_dir){
210			# new style, requires a real theme, but has no effect
211			$gen->initTheme($data_dir->getThemePath("seashell.theme"));
212		} else {
213			# old style, anything works.
214			$gen->initTheme("/dev/null");
215		}
216		$gen->loadLanguage($langfile); # must come after initTheme
217		$gen->setEncoding("utf-8");
218		$highlighters{$langfile}=$gen;
219	}
220	else {
221		$gen=$highlighters{$langfile};
222	}
223
224	return "<div class=\"highlight-$extorfile\">".$gen->generateString($input)."</div>";
225}
226
2271
228