]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blobdiff - IkiWiki.pm
refactor sortspec translation
[git.ikiwiki.info.git] / IkiWiki.pm
index 00eadfd9818e6e41adc36457b6bb45361355817b..1730e476ae039ee2ff26241b810393184d648654 100644 (file)
@@ -7,14 +7,14 @@ use strict;
 use Encode;
 use HTML::Entities;
 use URI::Escape q{uri_escape_utf8};
-use POSIX;
+use POSIX ();
 use Storable;
 use open qw{:utf8 :std};
 
 use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
            %pagestate %wikistate %renderedfiles %oldrenderedfiles
            %pagesources %destsources %depends %depends_simple %hooks
-           %forcerebuild %loaded_plugins};
+           %forcerebuild %loaded_plugins %typedlinks %oldtypedlinks};
 
 use Exporter q{import};
 our @EXPORT = qw(hook debug error template htmlpage deptype
@@ -24,7 +24,7 @@ our @EXPORT = qw(hook debug error template htmlpage deptype
                 add_underlay pagetitle titlepage linkpage newpagefile
                 inject add_link
                  %config %links %pagestate %wikistate %renderedfiles
-                 %pagesources %destsources);
+                 %pagesources %destsources %typedlinks);
 our $VERSION = 3.00; # plugin interface version, next is ikiwiki version
 our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
 our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
@@ -37,6 +37,7 @@ our $DEPEND_LINKS=4;
 # Optimisation.
 use Memoize;
 memoize("abs2rel");
+memoize("sortspec_translate");
 memoize("pagespec_translate");
 memoize("template_file");
 
@@ -334,11 +335,20 @@ sub getsetup () {
                safe => 0, # paranoia
                rebuild => 0,
        },
+       include => {
+               type => "string",
+               default => undef,
+               example => '^\.htaccess$',
+               description => "regexp of normally excluded files to include",
+               advanced => 1,
+               safe => 0, # regexp
+               rebuild => 1,
+       },
        exclude => {
                type => "string",
                default => undef,
-               example => '\.wav$',
-               description => "regexp of source files to ignore",
+               example => '^(*\.private|Makefile)$',
+               description => "regexp of files that should be skipped",
                advanced => 1,
                safe => 0, # regexp
                rebuild => 1,
@@ -458,6 +468,13 @@ sub getsetup () {
                safe => 0,
                rebuild => 0,
        },
+       setuptype => {
+               type => "internal",
+               default => "Standard",
+               description => "perl class to use to dump setup file",
+               safe => 0,
+               rebuild => 0,
+       },
        allow_symlinks_before_srcdir => {
                type => "boolean",
                default => 0,
@@ -1131,7 +1148,7 @@ sub openiduser ($) {
                # Convert "http://somehost.com/user" to "user [somehost.com]".
                # (also "https://somehost.com/user/")
                if ($display !~ /\[/) {
-                       $display=~s/^https?:\/\/(.+)\/([^\/]+)\/?$/$2 [$1]/;
+                       $display=~s/^https?:\/\/(.+)\/([^\/#?]+)\/?(?:[#?].*)?$/$2 [$1]/;
                }
                $display=~s!^https?://!!; # make sure this is removed
                eval q{use CGI 'escapeHTML'};
@@ -1148,7 +1165,7 @@ sub htmlize ($$$$) {
        my $content=shift;
        
        my $oneline = $content !~ /\n/;
-
+       
        if (exists $hooks{htmlize}{$type}) {
                $content=$hooks{htmlize}{$type}{call}->(
                        page => $page,
@@ -1169,10 +1186,9 @@ sub htmlize ($$$$) {
        
        if ($oneline) {
                # hack to get rid of enclosing junk added by markdown
-               # and other htmlizers
+               # and other htmlizers/sanitizers
                $content=~s/^<p>//i;
-               $content=~s/<\/p>$//i;
-               chomp $content;
+               $content=~s/<\/p>\n*$//i;
        }
 
        return $content;
@@ -1487,7 +1503,7 @@ sub loadindex () {
        if (! $config{rebuild}) {
                %pagesources=%pagemtime=%oldlinks=%links=%depends=
                %destsources=%renderedfiles=%pagecase=%pagestate=
-               %depends_simple=();
+               %depends_simple=%typedlinks=%oldtypedlinks=();
        }
        my $in;
        if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
@@ -1553,6 +1569,14 @@ sub loadindex () {
                        if (exists $d->{state}) {
                                $pagestate{$page}=$d->{state};
                        }
+                       if (exists $d->{typedlinks}) {
+                               $typedlinks{$page}=$d->{typedlinks};
+
+                               while (my ($type, $links) = each %{$typedlinks{$page}}) {
+                                       next unless %$links;
+                                       $oldtypedlinks{$page}{$type} = {%$links};
+                               }
+                       }
                }
                $oldrenderedfiles{$page}=[@{$d->{dest}}];
        }
@@ -1601,6 +1625,10 @@ sub saveindex () {
                        $index{page}{$src}{depends_simple} = $depends_simple{$page};
                }
 
+               if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+                       $index{page}{$src}{typedlinks} = $typedlinks{$page};
+               }
+
                if (exists $pagestate{$page}) {
                        foreach my $id (@hookids) {
                                foreach my $key (keys %{$pagestate{$page}{$id}}) {
@@ -1780,7 +1808,7 @@ sub add_depends ($$;$) {
 
        # Add explicit dependencies for influences.
        my $sub=pagespec_translate($pagespec);
-       return if $@;
+       return unless defined $sub;
        foreach my $p (keys %pagesources) {
                my $r=$sub->($p, location => $page);
                my $i=$r->influences;
@@ -1810,6 +1838,7 @@ sub deptype (@) {
        return $deptype;
 }
 
+my $file_prune_regexp;
 sub file_pruned ($;$) {
        my $file=shift;
        if (@_) {
@@ -1820,8 +1849,15 @@ sub file_pruned ($;$) {
                $file =~ s#^\Q$base\E/+##;
        }
 
-       my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
-       return $file =~ m/$regexp/;
+       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 () {
@@ -1902,12 +1938,82 @@ sub inject {
        use warnings;
 }
 
-sub add_link ($$) {
+sub add_link ($$;$) {
        my $page=shift;
        my $link=shift;
+       my $type=shift;
 
        push @{$links{$page}}, $link
                unless grep { $_ eq $link } @{$links{$page}};
+
+       if (defined $type) {
+               $typedlinks{$page}{$type}{$link} = 1;
+       }
+}
+
+sub sortspec_translate ($$) {
+       my $spec = shift;
+       my $reverse = shift;
+
+       my $code = "";
+       my @data;
+       while ($spec =~ m{
+               \s*
+               (-?)            # group 1: perhaps negated
+               \s*
+               (               # group 2: a word
+                       \w+\([^\)]*\)   # command(params)
+                       |
+                       [^\s]+          # or anything else
+               )
+               \s*
+       }gx) {
+               my $negated = $1;
+               my $word = $2;
+               my $params = undef;
+
+               if ($word =~ m/^(\w+)\((.*)\)$/) {
+                       # command with parameters
+                       $params = $2;
+                       $word = $1;
+               }
+               elsif ($word !~ m/^\w+$/) {
+                       error(sprintf(gettext("invalid sort type %s"), $word));
+               }
+
+               if (length $code) {
+                       $code .= " || ";
+               }
+
+               if ($negated) {
+                       $code .= "-";
+               }
+
+               if (exists $IkiWiki::SortSpec::{"cmp_$word"}) {
+                       if (defined $params) {
+                               push @data, $params;
+                               $code .= "IkiWiki::SortSpec::cmp_$word(\$data[$#data])";
+                       }
+                       else {
+                               $code .= "IkiWiki::SortSpec::cmp_$word(undef)";
+                       }
+               }
+               else {
+                       error(sprintf(gettext("unknown sort type %s"), $word));
+               }
+       }
+
+       if (! length $code) {
+               # undefined sorting method... sort arbitrarily
+               return sub { 0 };
+       }
+
+       if ($reverse) {
+               $code="-($code)";
+       }
+
+       no warnings;
+       return eval 'sub { '.$code.' }';
 }
 
 sub pagespec_translate ($) {
@@ -1977,7 +2083,7 @@ sub pagespec_match ($$;@) {
 
        my $sub=pagespec_translate($spec);
        return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
-               if $@ || ! defined $sub;
+               if ! defined $sub;
        return $sub->($page, @params);
 }
 
@@ -1995,7 +2101,9 @@ sub pagespec_match_list ($$;@) {
 
        my $sub=pagespec_translate($pagespec);
        error "syntax error in pagespec \"$pagespec\""
-               if $@ || ! defined $sub;
+               if ! defined $sub;
+       my $sort=sortspec_translate($params{sort}, $params{reverse})
+               if defined $params{sort};
 
        my @candidates;
        if (exists $params{list}) {
@@ -2008,39 +2116,19 @@ sub pagespec_match_list ($$;@) {
                        ? 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
+       $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;
@@ -2062,14 +2150,21 @@ sub pagespec_match_list ($$;@) {
                $depends_simple{$page}{lc $k} |= $i->{$k};
        }
 
-       return @matches;
+       # 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;
 
-       my $sub=pagespec_translate($spec);
-       return ! $@;
+       return defined pagespec_translate($spec);
 }
 
 sub glob2re ($) {
@@ -2189,26 +2284,34 @@ sub match_link ($$;@) {
 
        $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", "" => 1)
+       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}) {
                if (length $bestlink) {
-                       return IkiWiki::SuccessReason->new("$page links to $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if $bestlink eq IkiWiki::bestlink($page, $p);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && $bestlink eq IkiWiki::bestlink($page, $p)) {
+                               return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+                       }
                }
                else {
-                       return IkiWiki::SuccessReason->new("$page links to page $p matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if match_glob($p, $link, %params);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && 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/^\///;
-                       return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if match_glob($p_rel, $link, %params);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p_rel}) && 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", "" => 1);
+       return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
 }
 
 sub match_backlink ($$;@) {
@@ -2343,4 +2446,22 @@ sub match_ip ($$;@) {
        }
 }
 
+package IkiWiki::SortSpec;
+
+# This is in the SortSpec namespace so that the $a and $b that sort() uses
+# are easily available in this namespace, for cmp functions to use them.
+sub sort_pages {
+       my $f=shift;
+       sort $f @_
+}
+
+sub cmp_title {
+       IkiWiki::pagetitle(IkiWiki::basename($a))
+       cmp
+       IkiWiki::pagetitle(IkiWiki::basename($b))
+}
+
+sub cmp_mtime { $IkiWiki::pagemtime{$b} <=> $IkiWiki::pagemtime{$a} }
+sub cmp_age { $IkiWiki::pagectime{$b} <=> $IkiWiki::pagectime{$a} }
+
 1