+sub rcs_recentchanges ($) {
+ $hooks{rcs}{rcs_recentchanges}{call}->(@_);
+}
+
+sub rcs_diff ($) {
+ $hooks{rcs}{rcs_diff}{call}->(@_);
+}
+
+sub rcs_getctime ($) {
+ $hooks{rcs}{rcs_getctime}{call}->(@_);
+}
+
+sub rcs_receive () {
+ $hooks{rcs}{rcs_receive}{call}->();
+}
+
+sub add_depends ($$;$) {
+ my $page=shift;
+ my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+
+ # Is the pagespec a simple page name?
+ if ($pagespec =~ /$config{wiki_file_regexp}/ &&
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
+ return 1;
+ }
+
+ # Add explicit dependencies for influences.
+ my $sub=pagespec_translate($pagespec);
+ return unless defined $sub;
+ foreach my $p (keys %pagesources) {
+ my $r=$sub->($p, location => $page);
+ my $i=$r->influences;
+ foreach my $k (keys %$i) {
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+ last if $r->influences_static;
+ }
+
+ $depends{$page}{$pagespec} |= $deptype;
+ return 1;
+}
+
+sub deptype (@) {
+ my $deptype=0;
+ foreach my $type (@_) {
+ if ($type eq 'presence') {
+ $deptype |= $DEPEND_PRESENCE;
+ }
+ elsif ($type eq 'links') {
+ $deptype |= $DEPEND_LINKS;
+ }
+ elsif ($type eq 'content') {
+ $deptype |= $DEPEND_CONTENT;
+ }
+ }
+ return $deptype;
+}
+
+my $file_prune_regexp;
+sub file_pruned ($;$) {
+ my $file=shift;
+ if (@_) {
+ require File::Spec;
+ $file=File::Spec->canonpath($file);
+ my $base=File::Spec->canonpath(shift);
+ return if $file eq $base;
+ $file =~ s#^\Q$base\E/+##;
+ }
+
+ if (defined $config{include} && length $config{include}) {
+ return 0 if $file =~ m/$config{include}/;
+ }
+
+ if (! defined $file_prune_regexp) {
+ $file_prune_regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
+ $file_prune_regexp=qr/$file_prune_regexp/;
+ }
+ return $file =~ m/$file_prune_regexp/;
+}
+
+sub define_gettext () {
+ # If translation is needed, redefine the gettext function to do it.
+ # Otherwise, it becomes a quick no-op.
+ my $gettext_obj;
+ my $getobj;
+ if ((exists $ENV{LANG} && length $ENV{LANG}) ||
+ (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
+ (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
+ $getobj=sub {
+ $gettext_obj=eval q{
+ use Locale::gettext q{textdomain};
+ Locale::gettext->domain('ikiwiki')
+ };
+ };
+ }
+
+ no warnings 'redefine';
+ *gettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->get(shift);
+ }
+ else {
+ return shift;
+ }
+ };
+ *ngettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->nget(@_);
+ }
+ else {
+ return ($_[2] == 1 ? $_[0] : $_[1])
+ }
+ };
+}
+
+sub gettext {
+ define_gettext();
+ gettext(@_);
+}
+
+sub ngettext {
+ define_gettext();
+ ngettext(@_);
+}
+
+sub yesno ($) {
+ my $val=shift;
+
+ return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
+}
+
+sub inject {
+ # Injects a new function into the symbol table to replace an
+ # exported function.
+ my %params=@_;
+
+ # This is deep ugly perl foo, beware.
+ no strict;
+ no warnings;
+ if (! defined $params{parent}) {
+ $params{parent}='::';
+ $params{old}=\&{$params{name}};
+ $params{name}=~s/.*:://;
+ }
+ my $parent=$params{parent};
+ foreach my $ns (grep /^\w+::/, keys %{$parent}) {
+ $ns = $params{parent} . $ns;
+ inject(%params, parent => $ns) unless $ns eq '::main::';
+ *{$ns . $params{name}} = $params{call}
+ if exists ${$ns}{$params{name}} &&
+ \&{${$ns}{$params{name}}} == $params{old};
+ }
+ use strict;
+ use warnings;
+}
+
+sub add_link ($$) {
+ my $page=shift;
+ my $link=shift;
+
+ push @{$links{$page}}, $link
+ unless grep { $_ eq $link } @{$links{$page}};
+}
+
+sub pagespec_translate ($) {
+ my $spec=shift;
+
+ # Convert spec to perl code.
+ my $code="";
+ my @data;
+ while ($spec=~m{
+ \s* # ignore whitespace
+ ( # 1: match a single word
+ \! # !
+ |
+ \( # (
+ |
+ \) # )
+ |
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s()]+ # any other text
+ )
+ \s* # ignore whitespace
+ }gx) {
+ 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"}) {
+ push @data, $2;
+ $code.="IkiWiki::PageSpec::match_$1(\$page, \$data[$#data], \@_)";
+ }
+ else {
+ push @data, qq{unknown function in pagespec "$word"};
+ $code.="IkiWiki::ErrorReason->new(\$data[$#data])";
+ }
+ }
+ else {
+ push @data, $word;
+ $code.=" IkiWiki::PageSpec::match_glob(\$page, \$data[$#data], \@_)";
+ }
+ }
+
+ if (! length $code) {
+ $code="IkiWiki::FailReason->new('empty pagespec')";
+ }
+
+ no warnings;
+ return eval 'sub { my $page=shift; '.$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 $sub=pagespec_translate($spec);
+ return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
+ if ! defined $sub;
+ return $sub->($page, @params);
+}
+
+sub pagespec_match_list ($$;@) {
+ my $page=shift;
+ my $pagespec=shift;
+ my %params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (ref $page) {
+ print STDERR "warning: a plugin (".caller().") is using pagespec_match_list in an obsolete way, and needs to be updated\n";
+ $params{list}=$page;
+ $page=$params{location}; # ugh!
+ }
+
+ my $sub=pagespec_translate($pagespec);
+ error "syntax error in pagespec \"$pagespec\""
+ if ! defined $sub;
+
+ my @candidates;
+ if (exists $params{list}) {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } @{$params{list}}
+ : @{$params{list}};
+ }
+ else {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } keys %pagesources
+ : keys %pagesources;
+ }
+
+ if (defined $params{sort}) {
+ my $f;
+ if ($params{sort} eq 'title') {
+ $f=sub { pagetitle(basename($a)) cmp pagetitle(basename($b)) };
+ }
+ elsif ($params{sort} eq 'title_natural') {
+ eval q{use Sort::Naturally};
+ if ($@) {
+ error(gettext("Sort::Naturally needed for title_natural sort"));
+ }
+ $f=sub { Sort::Naturally::ncmp(pagetitle(basename($a)), pagetitle(basename($b))) };
+ }
+ elsif ($params{sort} eq 'mtime') {
+ $f=sub { $pagemtime{$b} <=> $pagemtime{$a} };
+ }
+ elsif ($params{sort} eq 'age') {
+ $f=sub { $pagectime{$b} <=> $pagectime{$a} };
+ }
+ else {
+ error sprintf(gettext("unknown sort type %s"), $params{sort});
+ }
+ @candidates = sort { &$f } @candidates;
+ }
+
+ @candidates=reverse(@candidates) if $params{reverse};
+
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+
+ # clear params, remainder is passed to pagespec
+ my $num=$params{num};
+ delete @params{qw{num deptype reverse sort filter list}};
+
+ my @matches;
+ my $firstfail;
+ my $count=0;
+ my $accum=IkiWiki::SuccessReason->new();
+ foreach my $p (@candidates) {
+ my $r=$sub->($p, %params, location => $page);
+ error(sprintf(gettext("cannot match pages: %s"), $r))
+ if $r->isa("IkiWiki::ErrorReason");
+ $accum |= $r;
+ if ($r) {
+ push @matches, $p;
+ last if defined $num && ++$count == $num;
+ }
+ }
+
+ # Add simple dependencies for accumulated influences.
+ my $i=$accum->influences;
+ foreach my $k (keys %$i) {
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+
+ return @matches;
+}
+
+sub pagespec_valid ($) {
+ my $spec=shift;
+
+ return defined pagespec_translate($spec);
+}
+
+sub glob2re ($) {
+ my $re=quotemeta(shift);
+ $re=~s/\\\*/.*/g;
+ $re=~s/\\\?/./g;
+ return $re;
+}
+
+package IkiWiki::FailReason;
+
+use overload (
+ '""' => sub { $_[0][0] },
+ '0+' => sub { 0 },
+ '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ '&' => sub { $_[0]->merge_influences($_[1], 1); $_[0] },
+ '|' => sub { $_[1]->merge_influences($_[0]); $_[1] },
+ fallback => 1,
+);
+
+our @ISA = 'IkiWiki::SuccessReason';
+
+package IkiWiki::SuccessReason;
+
+use overload (
+ '""' => sub { $_[0][0] },
+ '0+' => sub { 1 },
+ '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
+ '&' => sub { $_[1]->merge_influences($_[0], 1); $_[1] },
+ '|' => sub { $_[0]->merge_influences($_[1]); $_[0] },
+ fallback => 1,
+);
+
+sub new {
+ my $class = shift;
+ my $value = shift;
+ return bless [$value, {@_}], $class;
+}
+
+sub influences {
+ my $this=shift;
+ $this->[1]={@_} if @_;
+ my %i=%{$this->[1]};
+ delete $i{""};
+ return \%i;
+}
+
+sub influences_static {
+ return ! $_[0][1]->{""};
+}
+
+sub merge_influences {
+ my $this=shift;
+ my $other=shift;
+ my $anded=shift;
+
+ if (! $anded || (($this || %{$this->[1]}) &&
+ ($other || %{$other->[1]}))) {
+ foreach my $influence (keys %{$other->[1]}) {
+ $this->[1]{$influence} |= $other->[1]{$influence};
+ }
+ }
+ else {
+ # influence blocker
+ $this->[1]={};
+ }
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
+
+package IkiWiki::PageSpec;
+
+sub derel ($$) {
+ my $path=shift;
+ my $from=shift;
+
+ if ($path =~ m!^\./!) {
+ $from=~s#/?[^/]+$## if defined $from;
+ $path=~s#^\./##;
+ $path="$from/$path" if length $from;
+ }
+
+ return $path;
+}
+
+sub match_glob ($$;@) {