From b86276ffed7ee001b35cd610e5d56e5afb4088cf Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@debian.org>
Date: Thu, 25 Mar 2010 23:31:53 +0000
Subject: [PATCH] Reimplement extensible sorting mechanisms, in the same way as
 pagespecs

---
 IkiWiki.pm              | 145 ++++++++++++++++++++++++----------------
 IkiWiki/Plugin/meta.pm  |  11 +--
 doc/plugins/write.mdwn  |  53 +++++++--------
 t/pagespec_match_list.t |   6 +-
 4 files changed, 120 insertions(+), 95 deletions(-)

diff --git a/IkiWiki.pm b/IkiWiki.pm
index ce8fdd454..a89c14058 100644
--- a/IkiWiki.pm
+++ b/IkiWiki.pm
@@ -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
diff --git a/IkiWiki/Plugin/meta.pm b/IkiWiki/Plugin/meta.pm
index a470041c9..e8cc1e392 100644
--- a/IkiWiki/Plugin/meta.pm
+++ b/IkiWiki/Plugin/meta.pm
@@ -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
diff --git a/doc/plugins/write.mdwn b/doc/plugins/write.mdwn
index 1010e76e4..de2b47015 100644
--- a/doc/plugins/write.mdwn
+++ b/doc/plugins/write.mdwn
@@ -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
diff --git a/t/pagespec_match_list.t b/t/pagespec_match_list.t
index 309961f1c..743ae4637 100755
--- a/t/pagespec_match_list.t
+++ b/t/pagespec_match_list.t
@@ -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",
-- 
2.39.5