chiark / gitweb /
disobedience/choose-search.c: Fix segfault when search terms change.
[disorder] / cgi / macros-disorder.c
index 8e891d6afcb2b3c8a066dc6f0171db783727fa08..1ef1ee1f3d0ed4f8e2db80197f0b0ecd6b87e41e 100644 (file)
@@ -1,25 +1,24 @@
-
 /*
  * This file is part of DisOrder.
  * Copyright (C) 2004-2008 Richard Kettlewell
  *
 /*
  * This file is part of DisOrder.
  * Copyright (C) 2004-2008 Richard Kettlewell
  *
- * This program is free software; you can redistribute it and/or modify
+ * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
+ * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
  *
  * (at your option) any later version.
  *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
  * You should have received a copy of the GNU General Public License
  * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
  */
-/** @file server/macros-disorder.c
+/** @file cgi/macros-disorder.c
  * @brief DisOrder-specific expansions
  * @brief DisOrder-specific expansions
+ *
+ * See @ref lib/macros-builtin.c for docstring syntax.
  */
 
 #include "disorder-cgi.h"
  */
 
 #include "disorder-cgi.h"
@@ -38,7 +37,7 @@ static const char *make_index(int i) {
   return s;
 }
 
   return s;
 }
 
-/*! @server-version
+/*$ @server-version
  *
  * Expands to the server's version string, or a (safe to use) error
  * value if the server is unavailable or broken.
  *
  * Expands to the server's version string, or a (safe to use) error
  * value if the server is unavailable or broken.
@@ -57,7 +56,7 @@ static int exp_server_version(int attribute((unused)) nargs,
   return sink_writes(output, cgi_sgmlquote(v)) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, cgi_sgmlquote(v)) < 0 ? -1 : 0;
 }
 
-/*! @version
+/*$ @version
  *
  * Expands to the local version string.
  */
  *
  * Expands to the local version string.
  */
@@ -69,7 +68,7 @@ static int exp_version(int attribute((unused)) nargs,
                      cgi_sgmlquote(disorder_short_version_string)) < 0 ? -1 : 0;
 }
 
                      cgi_sgmlquote(disorder_short_version_string)) < 0 ? -1 : 0;
 }
 
-/*! @url
+/*$ @url
  *
  * Expands to the base URL of the web interface.
  */
  *
  * Expands to the base URL of the web interface.
  */
@@ -81,7 +80,7 @@ static int exp_url(int attribute((unused)) nargs,
                      cgi_sgmlquote(config->url)) < 0 ? -1 : 0;
 }
 
                      cgi_sgmlquote(config->url)) < 0 ? -1 : 0;
 }
 
-/*! @arg{NAME}
+/*$ @arg{NAME}
  *
  * Expands to the UNQUOTED form of CGI argument NAME, or the empty string if
  * there is no such argument.  Use @argq for a quick way to quote the argument.
  *
  * Expands to the UNQUOTED form of CGI argument NAME, or the empty string if
  * there is no such argument.  Use @argq for a quick way to quote the argument.
@@ -98,7 +97,7 @@ static int exp_arg(int attribute((unused)) nargs,
     return 0;
 }
 
     return 0;
 }
 
-/*! @argq{NAME}
+/*$ @argq{NAME}
  *
  * Expands to the (quoted) form of CGI argument NAME, or the empty string if
  * there is no such argument.  Use @arg for the unquoted argument.
  *
  * Expands to the (quoted) form of CGI argument NAME, or the empty string if
  * there is no such argument.  Use @arg for the unquoted argument.
@@ -115,7 +114,7 @@ static int exp_argq(int attribute((unused)) nargs,
     return 0;
 }
 
     return 0;
 }
 
-/*! @user
+/*$ @user
  *
  * Expands to the logged-in username (which might be "guest"), or to
  * the empty string if not connected.
  *
  * Expands to the logged-in username (which might be "guest"), or to
  * the empty string if not connected.
@@ -131,7 +130,7 @@ static int exp_user(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @part{TRACK|ID}{PART}{CONTEXT}
+/*$ @part{TRACK|ID}{PART}{CONTEXT}
  *
  * Expands to a track name part.
  *
  *
  * Expands to a track name part.
  *
@@ -160,10 +159,11 @@ static int exp_part(int nargs,
       return 0;
   }
   if(dcgi_client
       return 0;
   }
   if(dcgi_client
-     && !disorder_part(dcgi_client, (char **)&s,
+     && !disorder_part(dcgi_client,
                        track,
                        !strcmp(context, "short") ? "display" : context,
                        track,
                        !strcmp(context, "short") ? "display" : context,
-                       part)) {
+                       part,
+                       (char **)&s)) {
     if(!strcmp(context, "short"))
       s = truncate_for_display(s, config->short_display);
     return sink_writes(output, cgi_sgmlquote(s)) < 0 ? -1 : 0;
     if(!strcmp(context, "short"))
       s = truncate_for_display(s, config->short_display);
     return sink_writes(output, cgi_sgmlquote(s)) < 0 ? -1 : 0;
@@ -171,7 +171,7 @@ static int exp_part(int nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @quote{STRING}
+/*$ @quote{STRING}
  *
  * SGML-quotes STRING.  Note that most expansion results are already suitable
  * quoted, so this expansion is usually not required.
  *
  * SGML-quotes STRING.  Note that most expansion results are already suitable
  * quoted, so this expansion is usually not required.
@@ -183,7 +183,7 @@ static int exp_quote(int attribute((unused)) nargs,
   return sink_writes(output, cgi_sgmlquote(args[0])) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, cgi_sgmlquote(args[0])) < 0 ? -1 : 0;
 }
 
-/*! @who{ID}
+/*$ @who{ID}
  *
  * Expands to the name of the submitter of track ID, which must be a playing
  * track, in the queue, or in the recent list.
  *
  * Expands to the name of the submitter of track ID, which must be a playing
  * track, in the queue, or in the recent list.
@@ -199,7 +199,7 @@ static int exp_who(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @when{ID}
+/*$ @when{ID}
  *
  * Expands to the time a track started or is expected to start.  The track must
  * be a playing track, in the queue, or in the recent list.
  *
  * Expands to the time a track started or is expected to start.  The track must
  * be a playing track, in the queue, or in the recent list.
@@ -236,7 +236,7 @@ static int exp_when(int attribute((unused)) nargs,
   return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
 }
 
   return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
 }
 
-/*! @length{ID|TRACK}
+/*$ @length{ID|TRACK}
  *
  * Expands to the length of a track, identified by its queue ID or its name.
  * If it is the playing track (identified by ID) then the amount played so far
  *
  * Expands to the length of a track, identified by its queue ID or its name.
  * If it is the playing track (identified by ID) then the amount played so far
@@ -268,7 +268,7 @@ static int exp_length(int attribute((unused)) nargs,
   return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
 }
 
   return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
 }
 
-/*! @removable{ID}
+/*$ @removable{ID}
  *
  * Expands to "true" if track ID is removable (or scratchable, if it is the
  * playing track) and "false" otherwise.
  *
  * Expands to "true" if track ID is removable (or scratchable, if it is the
  * playing track) and "false" otherwise.
@@ -288,7 +288,7 @@ static int exp_removable(int attribute((unused)) nargs,
                             (dcgi_rights, disorder_user(dcgi_client), q));
 }
 
                             (dcgi_rights, disorder_user(dcgi_client), q));
 }
 
-/*! @movable{ID}{DIR}
+/*$ @movable{ID}{DIR}
  *
  * Expands to "true" if track ID is movable and "false" otherwise.
  *
  *
  * Expands to "true" if track ID is movable and "false" otherwise.
  *
@@ -321,7 +321,7 @@ static int exp_movable(int  nargs,
                                       q));
 }
 
                                       q));
 }
 
-/*! @playing{TEMPLATE}
+/*$ @playing{TEMPLATE}
  *
  * Expands to TEMPLATE, with the following expansions:
  * - @id: the queue ID of the playing track
  *
  * Expands to TEMPLATE, with the following expansions:
  * - @id: the queue ID of the playing track
@@ -349,7 +349,7 @@ static int exp_playing(int nargs,
                    output, u);
 }
 
                    output, u);
 }
 
-/*! @queue{TEMPLATE}
+/*$ @queue{TEMPLATE}
  *
  * For each track in the queue, expands TEMPLATE with the following expansions:
  * - @id: the queue ID of the track
  *
  * For each track in the queue, expands TEMPLATE with the following expansions:
  * - @id: the queue ID of the track
@@ -381,7 +381,7 @@ static int exp_queue(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @recent{TEMPLATE}
+/*$ @recent{TEMPLATE}
  *
  * For each track in the recently played list, expands TEMPLATE with the
  * following expansions:
  *
  * For each track in the recently played list, expands TEMPLATE with the
  * following expansions:
@@ -414,7 +414,7 @@ static int exp_recent(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @new{TEMPLATE}
+/*$ @new{TEMPLATE}
  *
  * For each track in the newly added list, expands TEMPLATE wit the following
  * expansions:
  *
  * For each track in the newly added list, expands TEMPLATE wit the following
  * expansions:
@@ -448,7 +448,7 @@ static int exp_new(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @volume{CHANNEL}
+/*$ @volume{CHANNEL}
  *
  * Expands to the volume in a given channel.  CHANNEL must be "left" or
  * "right".
  *
  * Expands to the volume in a given channel.  CHANNEL must be "left" or
  * "right".
@@ -458,12 +458,12 @@ static int exp_volume(int attribute((unused)) nargs,
                       struct sink *output,
                       void attribute((unused)) *u) {
   dcgi_lookup(DCGI_VOLUME);
                       struct sink *output,
                       void attribute((unused)) *u) {
   dcgi_lookup(DCGI_VOLUME);
-  return sink_printf(output, "%d",
+  return sink_printf(output, "%ld",
                      !strcmp(args[0], "left")
                          ? dcgi_volume_left : dcgi_volume_right) < 0 ? -1 : 0;
 }
 
                      !strcmp(args[0], "left")
                          ? dcgi_volume_left : dcgi_volume_right) < 0 ? -1 : 0;
 }
 
-/*! @isplaying
+/*$ @isplaying
  *
  * Expands to "true" if there is a playing track, otherwise "false".
  */
  *
  * Expands to "true" if there is a playing track, otherwise "false".
  */
@@ -475,7 +475,7 @@ static int exp_isplaying(int attribute((unused)) nargs,
   return mx_bool_result(output, !!dcgi_playing);
 }
 
   return mx_bool_result(output, !!dcgi_playing);
 }
 
-/*! @isqueue
+/*$ @isqueue
  *
  * Expands to "true" if there the queue is nonempty, otherwise "false".
  */
  *
  * Expands to "true" if there the queue is nonempty, otherwise "false".
  */
@@ -487,7 +487,7 @@ static int exp_isqueue(int attribute((unused)) nargs,
   return mx_bool_result(output, !!dcgi_queue);
 }
 
   return mx_bool_result(output, !!dcgi_queue);
 }
 
-/*! @isrecent@
+/*$ @isrecent
  *
  * Expands to "true" if there the recently played list is nonempty, otherwise
  * "false".
  *
  * Expands to "true" if there the recently played list is nonempty, otherwise
  * "false".
@@ -500,7 +500,7 @@ static int exp_isrecent(int attribute((unused)) nargs,
   return mx_bool_result(output, !!dcgi_recent);
 }
 
   return mx_bool_result(output, !!dcgi_recent);
 }
 
-/*! @isnew
+/*$ @isnew
  *
  * Expands to "true" if there the newly added track list is nonempty, otherwise
  * "false".
  *
  * Expands to "true" if there the newly added track list is nonempty, otherwise
  * "false".
@@ -513,7 +513,7 @@ static int exp_isnew(int attribute((unused)) nargs,
   return mx_bool_result(output, !!dcgi_nnew);
 }
 
   return mx_bool_result(output, !!dcgi_nnew);
 }
 
-/*! @pref{TRACK}{KEY}
+/*$ @pref{TRACK}{KEY}
  *
  * Expands to a track preference.
  */
  *
  * Expands to a track preference.
  */
@@ -528,7 +528,7 @@ static int exp_pref(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @prefs{TRACK}{TEMPLATE}
+/*$ @prefs{TRACK}{TEMPLATE}
  *
  * For each track preference of track TRACK, expands TEMPLATE with the
  * following expansions:
  *
  * For each track preference of track TRACK, expands TEMPLATE with the
  * following expansions:
@@ -567,7 +567,7 @@ static int exp_prefs(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @transform{TRACK}{TYPE}{CONTEXT}
+/*$ @transform{TRACK}{TYPE}{CONTEXT}
  *
  * Transforms a track name (if TYPE is "track") or directory name (if type is
  * "dir").  CONTEXT should be the context, if it is left out then "display" is
  *
  * Transforms a track name (if TYPE is "track") or directory name (if type is
  * "dir").  CONTEXT should be the context, if it is left out then "display" is
@@ -582,7 +582,7 @@ static int exp_transform(int nargs,
   return sink_writes(output, cgi_sgmlquote(t)) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, cgi_sgmlquote(t)) < 0 ? -1 : 0;
 }
 
-/*! @enabled@
+/*$ @enabled
  *
  * Expands to "true" if playing is enabled, otherwise "false".
  */
  *
  * Expands to "true" if playing is enabled, otherwise "false".
  */
@@ -597,7 +597,7 @@ static int exp_enabled(int attribute((unused)) nargs,
   return mx_bool_result(output, e);
 }
 
   return mx_bool_result(output, e);
 }
 
-/*! @random-enabled
+/*$ @random-enabled
  *
  * Expands to "true" if random play is enabled, otherwise "false".
  */
  *
  * Expands to "true" if random play is enabled, otherwise "false".
  */
@@ -612,7 +612,7 @@ static int exp_random_enabled(int attribute((unused)) nargs,
   return mx_bool_result(output, e);
 }
 
   return mx_bool_result(output, e);
 }
 
-/*! @trackstate{TRACK}
+/*$ @trackstate{TRACK}
  *
  * Expands to "playing" if TRACK is currently playing, or "queue" if it is in
  * the queue, otherwise to nothing.
  *
  * Expands to "playing" if TRACK is currently playing, or "queue" if it is in
  * the queue, otherwise to nothing.
@@ -626,7 +626,7 @@ static int exp_trackstate(int attribute((unused)) nargs,
 
   if(!dcgi_client)
     return 0;
 
   if(!dcgi_client)
     return 0;
-  if(disorder_resolve(dcgi_client, &track, args[0]))
+  if(disorder_resolve(dcgi_client, args[0], &track))
     return 0;
   dcgi_lookup(DCGI_PLAYING);
   if(dcgi_playing && !strcmp(track, dcgi_playing->track))
     return 0;
   dcgi_lookup(DCGI_PLAYING);
   if(dcgi_playing && !strcmp(track, dcgi_playing->track))
@@ -638,7 +638,7 @@ static int exp_trackstate(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @thisurl
+/*$ @thisurl
  *
  * Expands to an UNQUOTED URL which points back to the current page.  (NB it
  * might not be byte for byte identical - for instance, CGI arguments might be
  *
  * Expands to an UNQUOTED URL which points back to the current page.  (NB it
  * might not be byte for byte identical - for instance, CGI arguments might be
@@ -651,7 +651,7 @@ static int exp_thisurl(int attribute((unused)) nargs,
   return sink_writes(output, cgi_thisurl(config->url)) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, cgi_thisurl(config->url)) < 0 ? -1 : 0;
 }
 
-/*! @resolve{TRACK}
+/*$ @resolve{TRACK}
  *
  * Expands to an UNQUOTED name for the TRACK that is not an alias, or to
  * nothing if it is not a valid track.
  *
  * Expands to an UNQUOTED name for the TRACK that is not an alias, or to
  * nothing if it is not a valid track.
@@ -662,12 +662,12 @@ static int exp_resolve(int attribute((unused)) nargs,
                        void attribute((unused)) *u) {
   char *r;
 
                        void attribute((unused)) *u) {
   char *r;
 
-  if(dcgi_client && !disorder_resolve(dcgi_client, &r, args[0]))
+  if(dcgi_client && !disorder_resolve(dcgi_client, args[0], &r))
     return sink_writes(output, r) < 0 ? -1 : 0;
   return 0;
 }
 
     return sink_writes(output, r) < 0 ? -1 : 0;
   return 0;
 }
 
-/*! @paused
+/*$ @paused
  *
  * Expands to "true" if the playing track is paused, to "false" if it is
  * playing (or if there is no playing track at all).
  *
  * Expands to "true" if the playing track is paused, to "false" if it is
  * playing (or if there is no playing track at all).
@@ -681,7 +681,7 @@ static int exp_paused(int attribute((unused)) nargs,
                                  && dcgi_playing->state == playing_paused));
 }
 
                                  && dcgi_playing->state == playing_paused));
 }
 
-/*! @state{ID}@
+/*$ @state{ID}
  *
  * Expands to the current state of track ID.
  */
  *
  * Expands to the current state of track ID.
  */
@@ -696,7 +696,22 @@ static int exp_state(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @right{RIGHT}{WITH-RIGHT}{WITHOUT-RIGHT}@
+/*$ @origin{ID}
+ *
+ * Expands to the current origin of track ID.
+ */
+static int exp_origin(int attribute((unused)) nargs,
+                      char **args,
+                      struct sink *output,
+                      void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+
+  if(q)
+    return sink_writes(output, track_origins[q->origin]) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*$ @right{RIGHT}{WITH-RIGHT}{WITHOUT-RIGHT}
  *
  * Expands to WITH-RIGHT if the current user has right RIGHT, otherwise to
  * WITHOUT-RIGHT.  The WITHOUT-RIGHT argument may be left out.
  *
  * Expands to WITH-RIGHT if the current user has right RIGHT, otherwise to
  * WITHOUT-RIGHT.  The WITHOUT-RIGHT argument may be left out.
@@ -733,7 +748,7 @@ static int exp_right(int nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @userinfo{PROPERTY}
+/*$ @userinfo{PROPERTY}
  *
  * Expands to the named property of the current user.
  */
  *
  * Expands to the named property of the current user.
  */
@@ -750,7 +765,7 @@ static int exp_userinfo(int attribute((unused)) nargs,
   return 0;
 }
 
   return 0;
 }
 
-/*! @error
+/*$ @error
  *
  * Expands to the latest error string.
  */
  *
  * Expands to the latest error string.
  */
@@ -762,7 +777,7 @@ static int exp_error(int attribute((unused)) nargs,
               < 0 ? -1 : 0;
 }
 
               < 0 ? -1 : 0;
 }
 
-/*! @status
+/*$ @status
  *
  * Expands to the latest status string.
  */
  *
  * Expands to the latest status string.
  */
@@ -774,7 +789,7 @@ static int exp_status(int attribute((unused)) nargs,
               < 0 ? -1 : 0;
 }
 
               < 0 ? -1 : 0;
 }
 
-/*! @image{NAME}
+/*$ @image{NAME}
  *
  * Expands to the URL of the image called NAME.
  *
  *
  * Expands to the URL of the image called NAME.
  *
@@ -814,15 +829,6 @@ static int exp_image(int attribute((unused)) nargs,
   return sink_writes(output, cgi_sgmlquote(url)) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, cgi_sgmlquote(url)) < 0 ? -1 : 0;
 }
 
-/** @brief Compare two @ref entry objects */
-int dcgi_compare_entry(const void *a, const void *b) {
-  const struct dcgi_entry *ea = a, *eb = b;
-
-  return compare_tracks(ea->sort, eb->sort,
-                       ea->display, eb->display,
-                       ea->track, eb->track);
-}
-
 /** @brief Implementation of exp_tracks() and exp_dirs() */
 static int exp__files_dirs(int nargs,
                            const struct mx_node **args,
 /** @brief Implementation of exp_tracks() and exp_dirs() */
 static int exp__files_dirs(int nargs,
                            const struct mx_node **args,
@@ -837,7 +843,7 @@ static int exp__files_dirs(int nargs,
   char **tracks, *dir, *re;
   int n, ntracks, rc;
   const struct mx_node *m;
   char **tracks, *dir, *re;
   int n, ntracks, rc;
   const struct mx_node *m;
-  struct dcgi_entry *e;
+  struct tracksort_data *tsd;
 
   if((rc = mx_expandstr(args[0], &dir, u, "argument #0 (DIR)")))
     return rc;
 
   if((rc = mx_expandstr(args[0], &dir, u, "argument #0 (DIR)")))
     return rc;
@@ -854,34 +860,39 @@ static int exp__files_dirs(int nargs,
   /* Get the list */
   if(fn(dcgi_client, dir, re, &tracks, &ntracks))
     return 0;
   /* Get the list */
   if(fn(dcgi_client, dir, re, &tracks, &ntracks))
     return 0;
-  /* Sort it.  NB trackname_transform() does not go to the server. */
-  /* TODO use tracksort_init */
-  e = xcalloc(ntracks, sizeof *e);
-  for(n = 0; n < ntracks; ++n) {
-    e[n].track = tracks[n];
-    e[n].sort = trackname_transform(type, tracks[n], "sort");
-    e[n].display = trackname_transform(type, tracks[n], "display");
+  if(type) {
+    /* Sort it.  NB trackname_transform() does not go to the server. */
+    tsd = tracksort_init(ntracks, tracks, type);
+    /* Expand the subsiduary templates.  We chuck in @sort and @display because
+     * it is particularly easy to do so. */
+    for(n = 0; n < ntracks; ++n)
+      if((rc = mx_expand(mx_rewritel(m,
+                                     "index", make_index(n),
+                                     "parity", n % 2 ? "odd" : "even",
+                                     "track", tsd[n].track,
+                                     "first", n == 0 ? "true" : "false",
+                                     "last", n + 1 == ntracks ? "false" : "true",
+                                     "sort", tsd[n].sort,
+                                     "display", tsd[n].display,
+                                     (char *)0),
+                         output, u)))
+        return rc;
+  } else {
+    for(n = 0; n < ntracks; ++n)
+      if((rc = mx_expand(mx_rewritel(m,
+                                     "index", make_index(n),
+                                     "parity", n % 2 ? "odd" : "even",
+                                     "track", tracks[n],
+                                     "first", n == 0 ? "true" : "false",
+                                     "last", n + 1 == ntracks ? "false" : "true",
+                                     (char *)0),
+                         output, u)))
+        return rc;
   }
   }
-  qsort(e, ntracks, sizeof (struct dcgi_entry), dcgi_compare_entry);
-  /* Expand the subsiduary templates.  We chuck in @sort and @display because
-   * it is particularly easy to do so. */
-  for(n = 0; n < ntracks; ++n)
-    if((rc = mx_expand(mx_rewritel(m,
-                                   "index", make_index(n),
-                                   "parity", n % 2 ? "odd" : "even",
-                                   "track", e[n].track,
-                                   "first", n == 0 ? "true" : "false",
-                                   "last", n + 1 == ntracks ? "false" : "true",
-                                   "sort", e[n].sort,
-                                   "display", e[n].display,
-                                   (char *)0),
-                       output, u)))
-      return rc;
   return 0;
   return 0;
-
 }
 
 }
 
-/*! @tracks{DIR}{RE}{TEMPLATE}
+/*$ @tracks{DIR}{RE}{TEMPLATE}
  *
  * For each track below DIR, expands TEMPLATE with the
  * following expansions:
  *
  * For each track below DIR, expands TEMPLATE with the
  * following expansions:
@@ -902,7 +913,7 @@ static int exp_tracks(int nargs,
   return exp__files_dirs(nargs, args, output, u, "track", disorder_files);
 }
 
   return exp__files_dirs(nargs, args, output, u, "track", disorder_files);
 }
 
-/*! @dirs{DIR}{RE}{TEMPLATE}
+/*$ @dirs{DIR}{RE}{TEMPLATE}
  *
  * For each directory below DIR, expands TEMPLATE with the
  * following expansions:
  *
  * For each directory below DIR, expands TEMPLATE with the
  * following expansions:
@@ -920,7 +931,7 @@ static int exp_dirs(int nargs,
                     const struct mx_node **args,
                     struct sink *output,
                     void *u) {
                     const struct mx_node **args,
                     struct sink *output,
                     void *u) {
-  return exp__files_dirs(nargs, args, output, u, "dir", disorder_directories);
+  return exp__files_dirs(nargs, args, output, u, "dir", disorder_dirs);
 }
 
 static int exp__search_shim(disorder_client *c, const char *terms,
 }
 
 static int exp__search_shim(disorder_client *c, const char *terms,
@@ -929,7 +940,7 @@ static int exp__search_shim(disorder_client *c, const char *terms,
   return disorder_search(c, terms, vecp, nvecp);
 }
 
   return disorder_search(c, terms, vecp, nvecp);
 }
 
-/*! @search{KEYWORDS}{TEMPLATE}
+/*$ @search{KEYWORDS}{TEMPLATE}
  *
  * For each track matching KEYWORDS, expands TEMPLATE with the
  * following expansions:
  *
  * For each track matching KEYWORDS, expands TEMPLATE with the
  * following expansions:
@@ -938,17 +949,15 @@ static int exp__search_shim(disorder_client *c, const char *terms,
  * - @parity: "even" or "odd" alternately
  * - @first: "true" on the first directory and "false" otherwise
  * - @last: "true" on the last directory and "false" otherwise
  * - @parity: "even" or "odd" alternately
  * - @first: "true" on the first directory and "false" otherwise
  * - @last: "true" on the last directory and "false" otherwise
- * - @sort: the sort key for this track
- * - @display: the UNQUOTED display string for this track
  */
 static int exp_search(int nargs,
                       const struct mx_node **args,
                       struct sink *output,
                       void *u) {
  */
 static int exp_search(int nargs,
                       const struct mx_node **args,
                       struct sink *output,
                       void *u) {
-  return exp__files_dirs(nargs, args, output, u, "track", exp__search_shim);
+  return exp__files_dirs(nargs, args, output, u, NULL, exp__search_shim);
 }
 
 }
 
-/*! @label{NAME}
+/*$ @label{NAME}
  *
  * Expands to label NAME from options.labels.  Undefined lables expand to the
  * last dot-separated component, e.g. X.Y.Z to Z.
  *
  * Expands to label NAME from options.labels.  Undefined lables expand to the
  * last dot-separated component, e.g. X.Y.Z to Z.
@@ -960,7 +969,7 @@ static int exp_label(int attribute((unused)) nargs,
   return sink_writes(output, option_label(args[0])) < 0 ? -1 : 0;
 }
 
   return sink_writes(output, option_label(args[0])) < 0 ? -1 : 0;
 }
 
-/*! @breadcrumbs{DIR}{TEMPLATE}
+/*$ @breadcrumbs{DIR}{TEMPLATE}
  *
  * Expands TEMPLATE for each directory in the path up to DIR, excluding the root
  * but including DIR itself, with the following expansions:
  *
  * Expands TEMPLATE for each directory in the path up to DIR, excluding the root
  * but including DIR itself, with the following expansions:
@@ -980,7 +989,7 @@ static int exp_breadcrumbs(int attribute((unused)) nargs,
     return rc;
   /* Reject relative paths */
   if(dir[0] != '/') {
     return rc;
   /* Reject relative paths */
   if(dir[0] != '/') {
-    error(0, "breadcrumbs: '%s' is a relative path", dir);
+    disorder_error(0, "breadcrumbs: '%s' is a relative path", dir);
     return 0;
   }
   /* Skip the root */
     return 0;
   }
   /* Skip the root */
@@ -1016,6 +1025,7 @@ void dcgi_expansions(void) {
   mx_register("label",  1, 1, exp_label);
   mx_register("length", 1, 1, exp_length);
   mx_register("movable", 1, 2, exp_movable);
   mx_register("label",  1, 1, exp_label);
   mx_register("length", 1, 1, exp_length);
   mx_register("movable", 1, 2, exp_movable);
+  mx_register("origin", 1, 1, exp_origin);
   mx_register("part", 2, 3, exp_part);
   mx_register("paused", 0, 0, exp_paused);
   mx_register("pref", 2, 2, exp_pref);
   mx_register("part", 2, 3, exp_part);
   mx_register("paused", 0, 0, exp_paused);
   mx_register("pref", 2, 2, exp_pref);