Reimplement extensible sorting mechanisms, in the same way as pagespecs

master
Simon McVittie 2010-03-25 23:31:53 +00:00
parent 60edd2dc31
commit b86276ffed
4 changed files with 120 additions and 95 deletions

View 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

View 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

View 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

View File

@ -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",