+sub hook (@) { # {{{
+ my %param=@_;
+
+ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
+ error 'hook requires type, call, and id parameters';
+ }
+
+ return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
+
+ $hooks{$param{type}}{$param{id}}=\%param;
+ return 1;
+} # }}}
+
+sub run_hooks ($$) { # {{{
+ # Calls the given sub for each hook of the given type,
+ # passing it the hook function to call.
+ my $type=shift;
+ my $sub=shift;
+
+ if (exists $hooks{$type}) {
+ my @deferred;
+ foreach my $id (keys %{$hooks{$type}}) {
+ if ($hooks{$type}{$id}{last}) {
+ push @deferred, $id;
+ next;
+ }
+ $sub->($hooks{$type}{$id}{call});
+ }
+ foreach my $id (@deferred) {
+ $sub->($hooks{$type}{$id}{call});
+ }
+ }
+
+ return 1;
+} #}}}
+
+sub globlist_to_pagespec ($) { #{{{
+ my @globlist=split(' ', shift);
+
+ my (@spec, @skip);
+ foreach my $glob (@globlist) {
+ if ($glob=~/^!(.*)/) {
+ push @skip, $glob;
+ }
+ else {
+ push @spec, $glob;
+ }
+ }
+
+ my $spec=join(' or ', @spec);
+ if (@skip) {
+ my $skip=join(' and ', @skip);
+ if (length $spec) {
+ $spec="$skip and ($spec)";
+ }
+ else {
+ $spec=$skip;
+ }
+ }
+ return $spec;
+} #}}}
+
+sub is_globlist ($) { #{{{
+ my $s=shift;
+ return ( $s =~ /[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or" );
+} #}}}
+
+sub safequote ($) { #{{{
+ my $s=shift;
+ $s=~s/[{}]//g;
+ return "q{$s}";
+} #}}}
+
+sub add_depends ($$) { #{{{
+ my $page=shift;
+ my $pagespec=shift;
+
+ if (! exists $depends{$page}) {
+ $depends{$page}=$pagespec;
+ }
+ else {
+ $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
+ }
+
+ return 1;
+} # }}}
+
+sub file_pruned ($$) { #{{{
+ require File::Spec;
+ my $file=File::Spec->canonpath(shift);
+ my $base=File::Spec->canonpath(shift);
+ $file =~ s#^\Q$base\E/+##;
+
+ my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
+ return $file =~ m/$regexp/ && $file ne $base;
+} #}}}
+
+sub gettext { #{{{
+ # Only use gettext in the rare cases it's needed.
+ if ((exists $ENV{LANG} && length $ENV{LANG}) ||
+ (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
+ (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
+ if (! $gettext_obj) {
+ $gettext_obj=eval q{
+ use Locale::gettext q{textdomain};
+ Locale::gettext->domain('ikiwiki')
+ };
+ if ($@) {
+ print STDERR "$@";
+ $gettext_obj=undef;
+ return shift;
+ }
+ }
+ return $gettext_obj->get(shift);
+ }
+ else {
+ return shift;
+ }
+} #}}}
+
+sub pagespec_merge ($$) { #{{{
+ my $a=shift;
+ my $b=shift;
+
+ return $a if $a eq $b;
+
+ # Support for old-style GlobLists.
+ if (is_globlist($a)) {
+ $a=globlist_to_pagespec($a);
+ }
+ if (is_globlist($b)) {
+ $b=globlist_to_pagespec($b);
+ }
+
+ return "($a) or ($b)";
+} #}}}
+
+sub pagespec_translate ($) { #{{{
+ # This assumes that $page is in scope in the function
+ # that evalulates the translated pagespec code.
+ my $spec=shift;
+
+ # Support for old-style GlobLists.
+ if (is_globlist($spec)) {
+ $spec=globlist_to_pagespec($spec);
+ }
+
+ # Convert spec to perl code.
+ my $code="";
+ while ($spec=~m{
+ \s* # ignore whitespace
+ ( # 1: match a single word
+ \! # !
+ |
+ \( # (
+ |
+ \) # )
+ |
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s()]+ # any other text
+ )
+ \s* # ignore whitespace
+ }igx) {
+ my $word=$1;
+ if (lc $word eq 'and') {
+ $code.=' &&';
+ }
+ elsif (lc $word eq 'or') {
+ $code.=' ||';
+ }
+ elsif ($word eq "(" || $word eq ")" || $word eq "!") {
+ $code.=' '.$word;
+ }
+ elsif ($word =~ /^(\w+)\((.*)\)$/) {
+ if (exists $IkiWiki::PageSpec::{"match_$1"}) {
+ $code.="IkiWiki::PageSpec::match_$1(\$page, ".safequote($2).", \@params)";
+ }
+ else {
+ $code.=' 0';
+ }
+ }
+ else {
+ $code.=" IkiWiki::PageSpec::match_glob(\$page, ".safequote($word).", \@params)";
+ }
+ }
+
+ return $code;
+} #}}}
+
+sub pagespec_match ($$;@) { #{{{
+ my $page=shift;
+ my $spec=shift;
+ my @params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (@params == 1) {
+ unshift @params, 'location';
+ }
+
+ my $ret=eval pagespec_translate($spec);
+ return IkiWiki::FailReason->new('syntax error') if $@;
+ return $ret;
+} #}}}
+
+package IkiWiki::FailReason;
+
+use overload ( #{{{
+ '""' => sub { ${$_[0]} },
+ '0+' => sub { 0 },
+ '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ fallback => 1,
+); #}}}
+
+sub new { #{{{
+ return bless \$_[1], $_[0];
+} #}}}
+
+package IkiWiki::SuccessReason;
+
+use overload ( #{{{
+ '""' => sub { ${$_[0]} },
+ '0+' => sub { 1 },
+ '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
+ fallback => 1,
+); #}}}
+
+sub new { #{{{
+ return bless \$_[1], $_[0];
+}; #}}}
+
+package IkiWiki::PageSpec;
+
+sub match_glob ($$;@) { #{{{