+ 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 $sort=sortspec_translate($params{sort}, $params{reverse})
+ if defined $params{sort};
+
+ 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;
+ }
+
+ # clear params, remainder is passed to pagespec
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+ my $num=$params{num};
+ delete @params{qw{num deptype reverse sort filter list}};
+
+ # when only the top matches will be returned, it's efficient to
+ # sort before matching to pagespec,
+ if (defined $num && defined $sort) {
+ @candidates=IkiWiki::SortSpec::sort_pages(
+ $sort, @candidates);
+ }
+
+ 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");
+ unless ($r || $r->influences_static) {
+ $r->remove_influence($p);
+ }
+ $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};
+ }
+
+ # when all matches will be returned, it's efficient to
+ # sort after matching
+ if (! defined $num && defined $sort) {
+ return IkiWiki::SortSpec::sort_pages(
+ $sort, @matches);
+ }
+ else {
+ 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]={};
+ }
+}
+
+sub remove_influence {
+ my $this=shift;
+ my $torm=shift;
+
+ delete $this->[1]{$torm};
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
+
+package IkiWiki::PageSpec;
+
+sub derel ($$) {
+ my $path=shift;
+ my $from=shift;
+
+ if ($path =~ m!^\.(/|$)!) {
+ if ($1) {
+ $from=~s#/?[^/]+$## if defined $from;
+ $path=~s#^\./##;
+ $path="$from/$path" if defined $from && length $from;
+ }
+ else {
+ $path = $from;
+ $path = "" unless defined $path;
+ }
+ }
+
+ return $path;
+}
+
+sub match_glob ($$;@) {
+ my $page=shift;
+ my $glob=shift;
+ my %params=@_;
+
+ $glob=derel($glob, $params{location});
+
+ my $regexp=IkiWiki::glob2re($glob);
+ if ($page=~/^$regexp$/i) {
+ if (! IkiWiki::isinternal($page) || $params{internal}) {
+ return IkiWiki::SuccessReason->new("$glob matches $page");
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
+ }
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob does not match $page");
+ }
+}
+
+sub match_internal ($$;@) {
+ return match_glob(shift, shift, @_, internal => 1)
+}
+
+sub match_page ($$;@) {
+ my $page=shift;
+ my $match=match_glob($page, shift, @_);
+ if ($match) {
+ my $source=exists $IkiWiki::pagesources{$page} ?
+ $IkiWiki::pagesources{$page} :
+ $IkiWiki::delpagesources{$page};
+ my $type=defined $source ? IkiWiki::pagetype($source) : undef;
+ if (! defined $type) {
+ return IkiWiki::FailReason->new("$page is not a page");
+ }
+ }
+ return $match;
+}
+
+sub match_link ($$;@) {
+ my $page=shift;
+ my $link=lc(shift);
+ my %params=@_;
+
+ $link=derel($link, $params{location});
+ my $from=exists $params{location} ? $params{location} : '';
+ my $linktype=$params{linktype};
+ my $qualifier='';
+ if (defined $linktype) {
+ $qualifier=" with type $linktype";
+ }
+
+ my $links = $IkiWiki::links{$page};
+ return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ unless $links && @{$links};
+ my $bestlink = IkiWiki::bestlink($from, $link);
+ foreach my $p (@{$links}) {
+ next unless (! defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p});
+
+ if (length $bestlink) {
+ if ($bestlink eq IkiWiki::bestlink($page, $p)) {
+ return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ else {
+ if (match_glob($p, $link, %params)) {
+ return IkiWiki::SuccessReason->new("$page links to page $p$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ my ($p_rel)=$p=~/^\/?(.*)/;
+ $link=~s/^\///;
+ if (match_glob($p_rel, $link, %params)) {
+ return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ }
+ return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
+}
+
+sub match_backlink ($$;@) {
+ my $ret=match_link($_[1], $_[0], @_);
+ $ret->influences($_[1] => $IkiWiki::DEPEND_LINKS);
+ return $ret;
+}
+
+sub match_created_before ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_created_after ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_creation_day ($$;@) {
+ my $page=shift;
+ my $d=shift;
+ if ($d !~ /^\d+$/) {
+ return IkiWiki::ErrorReason->new("invalid day $d");
+ }
+ if ((localtime($IkiWiki::pagectime{$page}))[3] == $d) {
+ return IkiWiki::SuccessReason->new('creation_day matched');
+ }
+ else {
+ return IkiWiki::FailReason->new('creation_day did not match');
+ }
+}
+
+sub match_creation_month ($$;@) {
+ my $page=shift;
+ my $m=shift;
+ if ($m !~ /^\d+$/) {
+ return IkiWiki::ErrorReason->new("invalid month $m");
+ }
+ if ((localtime($IkiWiki::pagectime{$page}))[4] + 1 == $m) {
+ return IkiWiki::SuccessReason->new('creation_month matched');
+ }
+ else {
+ return IkiWiki::FailReason->new('creation_month did not match');
+ }
+}
+
+sub match_creation_year ($$;@) {
+ my $page=shift;
+ my $y=shift;
+ if ($y !~ /^\d+$/) {
+ return IkiWiki::ErrorReason->new("invalid year $y");
+ }
+ if ((localtime($IkiWiki::pagectime{$page}))[5] + 1900 == $y) {
+ return IkiWiki::SuccessReason->new('creation_year matched');
+ }
+ else {
+ return IkiWiki::FailReason->new('creation_year did not match');
+ }
+}
+
+sub match_user ($$;@) {
+ shift;
+ my $user=shift;
+ my %params=@_;
+
+ my $regexp=IkiWiki::glob2re($user);
+
+ if (! exists $params{user}) {
+ return IkiWiki::ErrorReason->new("no user specified");
+ }
+
+ if (defined $params{user} && $params{user}=~/^$regexp$/i) {
+ return IkiWiki::SuccessReason->new("user is $user");
+ }
+ elsif (! defined $params{user}) {
+ return IkiWiki::FailReason->new("not logged in");
+ }
+ else {
+ return IkiWiki::FailReason->new("user is $params{user}, not $user");
+ }
+}
+
+sub match_admin ($$;@) {
+ shift;
+ shift;
+ my %params=@_;
+
+ if (! exists $params{user}) {
+ return IkiWiki::ErrorReason->new("no user specified");
+ }
+
+ if (defined $params{user} && IkiWiki::is_admin($params{user})) {
+ return IkiWiki::SuccessReason->new("user is an admin");
+ }
+ elsif (! defined $params{user}) {
+ return IkiWiki::FailReason->new("not logged in");
+ }
+ else {
+ return IkiWiki::FailReason->new("user is not an admin");
+ }
+}
+
+sub match_ip ($$;@) {
+ shift;
+ my $ip=shift;
+ my %params=@_;
+
+ if (! exists $params{ip}) {
+ return IkiWiki::ErrorReason->new("no IP specified");
+ }
+
+ if (defined $params{ip} && lc $params{ip} eq lc $ip) {
+ return IkiWiki::SuccessReason->new("IP is $ip");
+ }
+ else {
+ return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
+ }
+}