Reimplement extensible sorting mechanisms, in the same way as pagespecs
authorSimon McVittie <smcv@debian.org>
Thu, 25 Mar 2010 23:31:53 +0000 (23:31 +0000)
committerSimon McVittie <smcv@debian.org>
Thu, 25 Mar 2010 23:31:53 +0000 (23:31 +0000)
IkiWiki.pm
IkiWiki/Plugin/meta.pm
doc/plugins/write.mdwn
t/pagespec_match_list.t

index ce8fdd454dacf3af686a3f49953ab0381ba429dc..a89c14058c0520bbb269a798882ea5a68b27f6f6 100644 (file)
@@ -37,6 +37,7 @@ our $DEPEND_LINKS=4;
 # Optimisation.
 use Memoize;
 memoize("abs2rel");
+memoize("cmpspec_translate");
 memoize("pagespec_translate");
 memoize("template_file");
 
@@ -1934,6 +1935,70 @@ sub add_link ($$) {
                unless grep { $_ eq $link } @{$links{$page}};
 }
 
+sub cmpspec_translate ($) {
+       my $spec = 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::PageSpec::{"cmp_$word"}) {
+                       if (exists $IkiWiki::PageSpec::{"check_cmp_$word"}) {
+                               $IkiWiki::PageSpec::{"check_cmp_$word"}->($params);
+                       }
+
+                       if (defined $params) {
+                               push @data, $params;
+                               $code .= "IkiWiki::PageSpec::cmp_$word(\@_, \$data[$#data])";
+                       }
+                       else {
+                               $code .= "IkiWiki::PageSpec::cmp_$word(\@_, undef)";
+                       }
+               }
+               else {
+                       error(sprintf(gettext("unknown sort type %s"), $word));
+               }
+       }
+
+       if (! length $code) {
+               # undefined sorting method... sort arbitrarily
+               return sub { 0 };
+       }
+
+       no warnings;
+       return eval 'sub { '.$code.' }';
+}
+
 sub pagespec_translate ($) {
        my $spec=shift;
 
@@ -2005,64 +2070,6 @@ sub pagespec_match ($$;@) {
        return $sub->($page, @params);
 }
 
-sub get_sort_function {
-       my $method = $_[0];
-
-       if ($method =~ m/\s/) {
-               my @methods = map { get_sort_function($_) } split(' ', $method);
-
-               return sub {
-                       foreach my $method (@methods) {
-                               my $answer = $method->($_[0], $_[1]);
-                               return $answer if $answer;
-                       }
-
-                       return 0;
-               };
-       }
-
-       my $sense = 1;
-
-       if ($method =~ s/^-//) {
-               $sense = -1;
-       }
-
-       my $token = $method;
-       my $parameter = undef;
-
-       if ($method =~ m/^(\w+)\((.*)\)$/) {
-               $token = $1;
-               $parameter = $2;
-       }
-
-       if (exists $hooks{sort}{$token}{call}) {
-               my $callback = $hooks{sort}{$token}{call};
-               return sub { $sense * $callback->($_[0], $_[1], $parameter) };
-       }
-
-       if ($method eq 'title') {
-               return sub { $sense * (pagetitle(basename($_[0])) cmp pagetitle(basename($_[1]))) };
-       }
-
-       if ($method eq 'title_natural') {
-               eval q{use Sort::Naturally};
-               if ($@) {
-                       error(gettext("Sort::Naturally needed for title_natural sort"));
-               }
-               return sub { $sense * Sort::Naturally::ncmp(pagetitle(basename($_[0])), pagetitle(basename($_[1]))) };
-       }
-
-       if ($method eq 'mtime') {
-               return sub { $sense * ($pagemtime{$_[1]} <=> $pagemtime{$_[0]}) };
-       }
-
-       if ($method eq 'age') {
-               return sub { $sense * ($pagectime{$_[1]} <=> $pagectime{$_[0]}) };
-       }
-
-       error sprintf(gettext("unknown sort type %s"), $method);
-}
-
 sub pagespec_match_list ($$;@) {
        my $page=shift;
        my $pagespec=shift;
@@ -2092,7 +2099,7 @@ sub pagespec_match_list ($$;@) {
        }
 
        if (defined $params{sort}) {
-               my $f = get_sort_function($params{sort});
+               my $f = cmpspec_translate($params{sort});
 
                @candidates = sort { $f->($a, $b) } @candidates;
        }
@@ -2407,4 +2414,24 @@ sub match_ip ($$;@) {
        }
 }
 
+sub cmp_title {
+       IkiWiki::pagetitle(IkiWiki::basename($_[0]))
+       cmp
+       IkiWiki::pagetitle(IkiWiki::basename($_[1]))
+}
+
+sub cmp_mtime { $IkiWiki::pagemtime{$_[1]} <=> $IkiWiki::pagemtime{$_[0]} }
+sub cmp_age { $IkiWiki::pagectime{$_[1]} <=> $IkiWiki::pagectime{$_[0]} }
+
+sub check_cmp_title_natural {
+       eval q{use Sort::Naturally};
+       if ($@) {
+               error(gettext("Sort::Naturally needed for title_natural sort"));
+       }
+}
+sub cmp_title_natural {
+       Sort::Naturally::ncmp(IkiWiki::pagetitle(IkiWiki::basename($_[0])),
+               IkiWiki::pagetitle(IkiWiki::basename($_[1])))
+}
+
 1
index a470041c97a528700b575e5b1f59c7c31a390764..e8cc1e3927a13c3f89f87fe27800a26355ddd006 100644 (file)
@@ -13,7 +13,6 @@ sub import {
        hook(type => "needsbuild", id => "meta", call => \&needsbuild);
        hook(type => "preprocess", id => "meta", call => \&preprocess, scan => 1);
        hook(type => "pagetemplate", id => "meta", call => \&pagetemplate);
-       hook(type => "sort", id => "meta_title", call => \&sort_meta_title);
 }
 
 sub getsetup () {
@@ -299,10 +298,6 @@ sub titlesort {
        return pagetitle(IkiWiki::basename($_[0]));
 }
 
-sub sort_meta_title {
-       return titlesort($_[0]) cmp titlesort($_[1]);
-}
-
 sub match {
        my $field=shift;
        my $page=shift;
@@ -353,4 +348,10 @@ sub match_copyright ($$;@) {
        IkiWiki::Plugin::meta::match("copyright", @_);
 }
 
+sub cmp_meta_title {
+       IkiWiki::Plugin::meta::titlesort($_[0])
+       cmp
+       IkiWiki::Plugin::meta::titlesort($_[1])
+}
+
 1
index 1010e76e400bab3e479c081873437d37db0095f1..de2b47015a6f5e8b4ce9a79178ef2a22edca76f4 100644 (file)
@@ -588,36 +588,6 @@ describes the plugin as a whole. For example:
 This hook is used to inject C code (which it returns) into the `main`
 function of the ikiwiki wrapper when it is being generated.
 
-### sort
-
-       hook(type => "sort", id => "foo", call => \&sort_by_foo);
-
-This hook adds an additional [[ikiwiki/pagespec/sorting]] order or overrides
-an existing one.
-
-The callback is given two page names followed by the parameter as arguments, and
-returns negative, zero or positive if the first page should come before,
-close to (i.e. undefined order), or after the second page.
-
-For instance, the built-in `title` sort order could be reimplemented as
-
-       sub sort_by_title {
-               pagetitle(basename($_[0])) cmp pagetitle(basename($_[1]));
-       }
-
-and to sort by an arbitrary `meta` value, you could use:
-
-       # usage: sort="meta(description)"
-       sub sort_by_meta {
-               my $param = $_[2];
-               error "sort=meta requires a parameter" unless defined $param;
-               my $left = $pagestate{$_[0]}{meta}{$param};
-               $left = "" unless defined $left;
-               my $right = $pagestate{$_[1]}{meta}{$param};
-               $right = "" unless defined $right;
-               return $left cmp $right;
-       }
-
 ## Exported variables
 
 Several variables are exported to your plugin when you `use IkiWiki;`
@@ -1140,6 +1110,29 @@ For example, "backlink(foo)" is influenced by the contents of page foo;
 they match; "created_before(foo)" is influenced by the metadata of foo;
 while "glob(*)" is not influenced by the contents of any page.
 
+### Sorting plugins
+
+Similarly, it's possible to write plugins that add new functions as
+[[ikiwiki/pagespec/sorting]] methods. To achieve this, add a function to
+the IkiWiki::PageSpec package named `cmp_foo`, which will be used when sorting
+by `foo` or `foo(...)` is requested.
+
+The function will be passed three or more parameters. The first two are
+page names, and the third is `undef` if invoked as `foo`, or the parameter
+`"bar"` if invoked as `foo(bar)`. It may also be passed additional, named
+parameters.
+
+It should return the same thing as Perl's `cmp` and `<=>` operators: negative
+if the first argument is less than the second, positive if the first argument
+is greater, or zero if they are considered equal. It may also raise an
+error using `error`, for instance if it needs a parameter but one isn't
+provided.
+
+You can also define a function called `check_cmp_foo` in the same package.
+If you do, it will be called while preparing to sort by `foo` or `foo(bar)`,
+with argument `undef` or `"bar"` respectively; it may raise an error using
+`error`, if sorting like that isn't going to work.
+
 ### Setup plugins
 
 The ikiwiki setup file is loaded using a pluggable mechanism. If you look
index 309961f1c52f78e57dbd5000b65257feda408443..743ae4637062d57b326e0d75e6486e2388549d9f 100755 (executable)
@@ -9,7 +9,11 @@ BEGIN { use_ok("IkiWiki"); }
 $config{srcdir}=$config{destdir}="/dev/null";
 IkiWiki::checkconfig();
 
-hook(type => "sort", id => "path", call => sub { $_[0] cmp $_[1] });
+{
+       package IkiWiki::PageSpec;
+
+       sub cmp_path { $_[0] cmp $_[1] }
+}
 
 %pagesources=(
        foo => "foo.mdwn",