/*********************************************** * * Copyright 2001 by Sean Conner. All Rights Reserved. * * 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 the Free Software Foundation; either version 2 * of the License, or (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. * * 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. * * Comments, questions and criticisms can be sent to: sean@conman.org * ************************************************/ #include #include #include #include #include #include #include "blogutil.h" #include "backend.h" /*****************************************************************/ static void swap_endpoints(tumbler__s *tum) { struct btm start; struct btm stop; assert(tum != NULL); assert(tum->ustart != UNIT_YEAR); assert(tum->ustop != UNIT_YEAR); /*-------------------------------------------------------------------- ; The best way to explain this is to give a bunch of examples. ; M = month D = day P = part ; ; 2000/12-09 MM ; 2000/12/01.1 2000/09/30.23 2000/09/01.1 2000/12/31.23 ; ; 2000/12-09/15 MD ; 2000/12/01.1 2000/09/15.23 2000/09/15.1 2000/12/31.23 ; ; 2000/12-09/15.2 MP ; 2000/12/01.1 2000/09/15.2 2000/09/15.2 2000/12/31.23 ; ; 2000/12/20-2000/09 DM ; 2000/12/20.1 2000/09/30.23 2000/09/01.1 2000/12/20.23 ; ; 2000/12/20-09/15 DD ; 2000/12/20.1 2000/09/15.23 2000/09/15.1 2000/12/20.23 ; ; 2000/12/20-09/15.2 DP ; 2000/12/20.1 2000/09/15.2 2000/09/15.2 2000/12/20.23 ; ; 2000/12/20.2-2000/09 PM ; 2000/12/20.2 2000/09/30.23 2000/09/01.1 2000/12/20.2 ; ; 2000/12/20.2-09/15 PD ; 2000/12/20.2 2000/09/15.23 2000/09/15.1 2000/12/20.2 ; ; 2000/12/20.2-09/15.3 PP ; 2000/12/20.2 2000/09/15.3 2000/09/15.3 2000/12/20.2 ; ; The year and the month are always swapped. ;--------------------------------------------------------------------*/ start.year = tum->stop.year; start.month = tum->stop.month; stop.year = tum->start.year; stop.month = tum->start.month; switch(tum->ustart) { case UNIT_MONTH: stop.day = max_monthday(stop.year,stop.month); stop.part = ENTRY_MAX; break; case UNIT_DAY: stop.day = tum->start.day; stop.part = ENTRY_MAX; break; case UNIT_PART: stop.day = tum->start.day; stop.part = tum->start.part; break; case UNIT_YEAR: assert(0); break; } switch(tum->ustop) { case UNIT_MONTH: start.day = 1; start.part = 1; break; case UNIT_DAY: start.day = tum->stop.day; start.part = 1; break; case UNIT_PART: start.day = tum->stop.day; start.part = tum->stop.part; break; case UNIT_YEAR: assert(0); break; } tum->start = start; tum->stop = stop; } /************************************************************************/ static struct btm calculate_previous(Blog *blog,Request *request,struct btm const start,unit__e navunit) { struct btm previous = start; switch(navunit) { case UNIT_YEAR: if (start.year == blog->first.year) request->f.navprev = false; else previous.year = start.year - 1; break; case UNIT_MONTH: if ( (start.year == blog->first.year) && (start.month == blog->first.month) ) request->f.navprev = false; else { btm_dec_month(&previous); previous.day = max_monthday(previous.year,previous.month); previous.part = 1; while(btm_cmp(&previous,&blog->first) >= 0) { BlogEntry *entry = BlogEntryRead(blog,&previous); if (entry == NULL) { btm_dec_day(&previous); continue; } assert(entry->valid); BlogEntryFree(entry); return previous; } request->f.navprev = false; } break; case UNIT_DAY: if (btm_cmp_date(&start,&blog->first) == 0) request->f.navprev = false; else { btm_dec_day(&previous); previous.part = 1; while(btm_cmp(&previous,&blog->first) >= 0) { BlogEntry *entry = BlogEntryRead(blog,&previous); if (entry == NULL) { btm_dec_day(&previous); continue; } assert(entry->valid); BlogEntryFree(entry); return previous; } request->f.navprev = false; } break; case UNIT_PART: if (btm_cmp(&start,&blog->first) == 0) request->f.navprev = false; else { btm_dec_part(&previous); while(btm_cmp(&previous,&blog->first) >= 0) { BlogEntry *entry = BlogEntryRead(blog,&previous); if (entry == NULL) { btm_dec_part(&previous); continue; } assert(entry->valid); BlogEntryFree(entry); return previous; } request->f.navprev = false; } break; } return previous; } /******************************************************************/ static struct btm calculate_next(Blog *blog,Request *request,struct btm const end,unit__e navunit) { struct btm next = end; switch(navunit) { case UNIT_YEAR: if (end.year == blog->now.year) request->f.navnext = false; else next.year = end.year + 1; break; case UNIT_MONTH: if ( (end.year == blog->now.year) && (end.month == blog->now.month) ) request->f.navnext = false; else { btm_inc_month(&next); next.day = 1; next.part = 1; while(btm_cmp(&next,&blog->now) <= 0) { BlogEntry *entry = BlogEntryRead(blog,&next); if (entry == NULL) { btm_inc_day(&next); continue; } assert(entry->valid); BlogEntryFree(entry); return next; } request->f.navnext = false; } break; case UNIT_DAY: if (btm_cmp_date(&end,&blog->now) == 0) request->f.navnext = false; else { btm_inc_day(&next); next.part = 1; while(btm_cmp(&next,&blog->now) <= 0) { BlogEntry *entry = BlogEntryRead(blog,&next); if (entry == NULL) { btm_inc_day(&next); continue; } assert(entry->valid); BlogEntryFree(entry); return next; } request->f.navnext = false; } break; case UNIT_PART: if (btm_cmp(&end,&blog->now) == 0) request->f.navnext = false; else { next.part++; while(btm_cmp(&next,&blog->now) <= 0) { BlogEntry *entry = BlogEntryRead(blog,&next); if (entry == NULL) { next.part = 1; btm_inc_day(&next); continue; } assert(entry->valid); BlogEntryFree(entry); return next; } request->f.navnext = false; } break; } return next; } /******************************************************************/ static int dstring_cmp(void const *needle,void const *haystack) { char const *key = needle; struct dstring const *value = haystack; assert(needle != NULL); assert(haystack != NULL); return strcmp(key,value->s1); } static char const *mime_type(char const *filename) { static struct dstring const types[] = { { ".ai" , "application/postscript" } , { ".aif" , "audio/x-aiff" } , { ".aifc" , "audio/x-aiff" } , { ".aiff" , "audio/x-aiff" } , { ".asc" , "text/plain" } , { ".au" , "audio/basic" } , { ".avi" , "video/x-msvideo" } , { ".bcpio" , "application/x-bcpio" } , { ".bin" , "application/octet-stream" } , { ".bmp" , "image/bmp" } , { ".cdf" , "application/x-netcdf" } , { ".cgm" , "image/cgm" } , { ".class" , "application/octet-stream" } , { ".cpio" , "application/x-cpio" } , { ".cpt" , "application/mac-compactpro" } , { ".csh" , "application/x-csh" } , { ".css" , "text/css" } , { ".dcr" , "application/x-director" } , { ".dir" , "application/x-director" } , { ".djv" , "image/vnd.djvu" } , { ".djvu" , "image/vnd.djvu" } , { ".dll" , "application/octet-stream" } , { ".dms" , "application/octet-stream" } , { ".doc" , "application/msword" } , { ".dtd" , "application/xml-dtd" } , { ".dvi" , "application/x-dvi" } , { ".dxr" , "application/x-director" } , { ".eps" , "application/postscript" } , { ".etx" , "text/x-setext" } , { ".exe" , "application/octet-stream" } , { ".ez" , "application/andrew-inset" } , { ".gif" , "image/gif" } , { ".gram" , "application/srgs" } , { ".grxml" , "application/srgs+xml" } , { ".gtar" , "application/x-gtar" } , { ".gz" , "application/x-gzip" } , { ".hdf" , "application/x-hdf" } , { ".hqx" , "application/mac-binhex40" } , { ".htm" , "text/html" } , { ".html" , "text/html" } , { ".ice" , "x-conference/x-cooltalk" } , { ".ico" , "image/x-icon" } , { ".ics" , "text/calendar" } , { ".ief" , "image/ief" } , { ".ifb" , "text/calendar" } , { ".iges" , "model/iges" } , { ".igs" , "model/iges" } , { ".jpe" , "image/jpeg" } , { ".jpeg" , "image/jpeg" } , { ".jpg" , "image/jpeg" } , { ".js" , "application/x-javascript" } , { ".kar" , "audio/midi" } , { ".latex" , "application/x-latex" } , { ".lha" , "application/octet-stream" } , { ".lzh" , "application/octet-stream" } , { ".m3u" , "audio/x-mpegurl" } , { ".m4v" , "video/x-m4v" } , { ".man" , "application/x-troff-man" } , { ".mathml" , "application/mathml+xml" } , { ".me" , "application/x-troff-me" } , { ".mesh" , "model/mesh" } , { ".mid" , "audio/midi" } , { ".midi" , "audio/midi" } , { ".mif" , "application/vnd.mif" } , { ".mov" , "video/quicktime" } , { ".movie" , "video/x-sgi-movie" } , { ".mp2" , "audio/mpeg" } , { ".mp3" , "audio/mpeg" } , { ".mp4" , "video/mp4" } , { ".mpe" , "video/mpeg" } , { ".mpeg" , "video/mpeg" } , { ".mpg" , "video/mpeg" } , { ".mpga" , "audio/mpeg" } , { ".ms" , "application/x-troff-ms" } , { ".msh" , "model/mesh" } , { ".mxu" , "video/vnd.mpegurl" } , { ".nc" , "application/x-netcdf" } , { ".oda" , "application/oda" } , { ".ogg" , "application/ogg" } , { ".pbm" , "image/x-portable-bitmap" } , { ".pdb" , "chemical/x-pdb" } , { ".pdf" , "application/pdf" } , { ".pgm" , "image/x-portable-graymap" } , { ".pgn" , "application/x-chess-pgn" } , { ".png" , "image/png" } , { ".pnm" , "image/x-portable-anymap" } , { ".ppm" , "image/x-portable-pixmap" } , { ".ppt" , "application/vnd.ms-powerpoint" } , { ".ps" , "application/postscript" } , { ".qt" , "video/quicktime" } , { ".ra" , "audio/x-realaudio" } , { ".ram" , "audio/x-pn-realaudio" } , { ".ras" , "image/x-cmu-raster" } , { ".rdf" , "application/rdf+xml" } , { ".rgb" , "image/x-rgb" } , { ".rm" , "audio/x-pn-realaudio" } , { ".roff" , "application/x-troff" } , { ".rpm" , "audio/x-pn-realaudio-plugin" } , { ".rtf" , "text/rtf" } , { ".rtx" , "text/richtext" } , { ".sgm" , "text/sgml" } , { ".sgml" , "text/sgml" } , { ".sh" , "application/x-sh" } , { ".shar" , "application/x-shar" } , { ".silo" , "model/mesh" } , { ".sit" , "application/x-stuffit" } , { ".skd" , "application/x-koan" } , { ".skm" , "application/x-koan" } , { ".skp" , "application/x-koan" } , { ".skt" , "application/x-koan" } , { ".smi" , "application/smil" } , { ".smil" , "application/smil" } , { ".snd" , "audio/basic" } , { ".so" , "application/octet-stream" } , { ".spl" , "application/x-futuresplash" } , { ".src" , "application/x-wais-source" } , { ".sv4cpio" , "application/x-sv4cpio" } , { ".sv4crc" , "application/x-sv4crc" } , { ".svg" , "image/svg+xml" } , { ".swf" , "application/x-shockwave-flash" } , { ".t" , "application/x-troff" } , { ".tar" , "application/x-tar" } , { ".tcl" , "application/x-tcl" } , { ".tex" , "application/x-tex" } , { ".texi" , "application/x-texinfo" } , { ".texinfo" , "application/x-texinfo" } , { ".tif" , "image/tiff" } , { ".tiff" , "image/tiff" } , { ".tr" , "application/x-troff" } , { ".tsv" , "text/tab-separated-values" } , { ".txt" , "text/plain" } , { ".ustar" , "application/x-ustar" } , { ".vcd" , "application/x-cdlink" } , { ".vrml" , "model/vrml" } , { ".vxml" , "application/voicexml+xml" } , { ".wav" , "audio/x-wav" } , { ".wbmp" , "image/vnd.wap.wbmp" } , { ".wbxml" , "application/vnd.wap.wbxml" } , { ".wml" , "text/vnd.wap.wml" } , { ".wmlc" , "application/vnd.wap.wmlc" } , { ".wmls" , "text/vnd.wap.wmlscript" } , { ".wmlsc" , "application/vnd.wap.wmlscriptc" } , { ".wrl" , "model/vrml" } , { ".x-html" , "text/x-html" } , { ".xbm" , "image/x-xbitmap" } , { ".xht" , "application/xhtml+xml" } , { ".xhtml" , "application/xhtml+xml" } , { ".xls" , "application/vnd.ms-excel" } , { ".xml" , "application/xml" } , { ".xpm" , "image/x-xpixmap" } , { ".xsl" , "application/xml" } , { ".xslt" , "application/xslt+xml" } , { ".xul" , "application/vnd.mozilla.xul+xml" } , { ".xwd" , "image/x-xwindowdump" } , { ".xyz" , "chemical/x-xyz" } , { ".zip" , "application/zip" } , }; char const *ext; assert(filename != NULL); ext = strrchr(filename,'.'); if (ext != NULL) { struct dstring const *v = bsearch( ext, types, sizeof(types)/sizeof(struct dstring), sizeof(struct dstring), dstring_cmp ); if (v != NULL) return v->s2; } return "text/plain"; } /******************************************************************/ static int display_file( tumbler__s const *spec, Blog *blog, Request *request, int (*errorf)(Blog *,Request *,int,char const *,...) ) { char fname[FILENAME_MAX]; char lastmod[64]; assert(spec != NULL); assert(errorf); snprintf( fname, sizeof(fname), "%4d/%02d/%02d/%s", spec->start.year, spec->start.month, spec->start.day, spec->filename ); if (request->f.cgiget) { struct stat status; char const *type; int rc; rc = stat(fname,&status); if (rc == -1) { if (errno == ENOENT) return (*errorf)(blog,request,HTTP_NOTFOUND,"%s: %s",fname,strerror(errno)); else if (errno == EACCES) return (*errorf)(blog,request,HTTP_FORBIDDEN,"%s: %s",fname,strerror(errno)); else return (*errorf)(blog,request,HTTP_ISERVERERR,"%s: %s",fname,strerror(errno)); } if (HttpNotModified(status.st_mtime)) { fprintf( stdout, "Status: %d\r\n" "Content-Length: 0\r\n" "\r\n", HTTP_NOTMODIFIED ); return 0; } if (freopen(fname,"r",stdin) == NULL) return (*errorf)(blog,request,HTTP_NOTFOUND,"%s: some internal error",fname); type = mime_type(spec->filename); if (strcmp(type,"text/x-html") == 0) { struct callback_data cbd; request->f.htmldump = true; generic_main(stdout,callback_init(&cbd,blog,request)); } else { fprintf( stdout, "Status: %d\r\n" "Content-Type: %s\r\n" "Content-Length: %lu\r\n" "Last-Modified: %s\r\n" "\r\n", HTTP_OKAY, type, (unsigned long)status.st_size, HttpTimeStamp(lastmod,sizeof(lastmod),status.st_mtime) ); fcopy(stdout,stdin); } fclose(stdin); } else fprintf(stdout,"File to open: %s\n",fname); return 0; } /*****************************************************************/ static char *tag_collect(List *list,char const *def) { BlogEntry *entry; /*----------------------------------------------------------------------- ; this function used to collect all the class tags from all the entries, ; but I think any advertising that's placed near the top of the page will ; do better if it's based off the first entry to be displayed. ;-----------------------------------------------------------------------*/ assert(list != NULL); assert(def != NULL); entry = (BlogEntry *)ListGetHead(list); if (NodeValid(&entry->node)) { if (!empty_string(entry->adtag)) return strdup(entry->adtag); if (!empty_string(entry->class)) return strdup(entry->class); } return strdup(def); } /********************************************************************/ static char *tag_pick(char const *tag,char const *def) { String *pool; size_t num; char *pick; assert(tag != NULL); assert(def != NULL); if (empty_string(tag)) return strdup(def); pool = tag_split(&num,tag); /*------------------------------------------------------------------------ ; if num is 0, then the tag string was malformed (basically, started with ; a ',') and therefore, we fall back to the default adtag. ;-----------------------------------------------------------------------*/ if (num) { unsigned int seed; FILE *fp; size_t r; fp = fopen("/dev/urandom","rb"); if (fp != NULL) { fread(&seed,sizeof(seed),1,fp); fclose(fp); } srand(seed); r = (((double)rand() / (double)RAND_MAX) * (double)num); assert(r < num); pick = fromstring(pool[r]); } else pick = strdup(def); free(pool); return pick; } /******************************************************************/ static void free_entries(List *list) { assert(list != NULL); for ( BlogEntry *entry = (BlogEntry *)ListRemHead(list); NodeValid(&entry->node); entry = (BlogEntry *)ListRemHead(list) ) { assert(entry->valid); BlogEntryFree(entry); } } /******************************************************************/ struct callback_data *callback_init(struct callback_data *cbd,Blog *blog,Request const *request) { assert(cbd != NULL); assert(blog != NULL); assert(request != NULL); ListInit(&cbd->list); cbd->entry = NULL; cbd->ad = NULL; cbd->adtag = NULL; cbd->adcat = NULL; cbd->wm = NULL; cbd->wmtitle = NULL; cbd->wmurl = NULL; cbd->navunit = UNIT_PART; cbd->status = HTTP_OKAY; cbd->template = &blog->config.templates[0]; /* XXX probably document this */ cbd->request = request; cbd->blog = blog; memset(&cbd->last, 0,sizeof(cbd->last)); memset(&cbd->previous,0,sizeof(cbd->previous)); memset(&cbd->next, 0,sizeof(cbd->next)); return cbd; } /************************************************************************/ pagegen__f TO_pagegen(char const *name) { assert(name != NULL); if (strcmp(name,"items") == 0) return pagegen_items; else if (strcmp(name,"days") == 0) return pagegen_days; else { assert(0); return pagegen_items; } } /************************************************************************/ int generate_thisday(Blog *blog,Request *request,FILE *out,struct btm when) { struct callback_data cbd; char *tags; assert(blog != NULL); assert(request != NULL); assert(out != NULL); callback_init(&cbd,blog,request); for(when.year = blog->first.year ; when.year <= blog->now.year ; when.year++) { if (btm_cmp_date(&when,&blog->first) < 0) continue; for (when.part = 1 ; ; when.part++) { BlogEntry *entry = BlogEntryRead(blog,&when); if (entry) { assert(entry->valid); ListAddTail(&cbd.list,&entry->node); } else break; } } tags = tag_collect(&cbd.list,blog->config.adtag); cbd.adtag = tag_pick(tags,blog->config.adtag); free(tags); generic_main(out,&cbd); free_entries(&cbd.list); free(cbd.adtag); return 0; } /************************************************************************/ int generate_pages(Blog *blog,Request *request) { assert(blog != NULL); assert(request != NULL); for (size_t i = 0 ; i < blog->config.templatenum ; i++) { FILE *out = fopen(blog->config.templates[i].file,"w"); int (*pagegen)(Blog *,Request *,struct template const *,FILE *); if (out == NULL) { syslog(LOG_ERR,"%s: %s",blog->config.templates[i].file,strerror(errno)); continue; } pagegen = TO_pagegen(blog->config.templates[i].pagegen); (*pagegen)(blog,request,&blog->config.templates[i],out); fclose(out); if (blog->config.templates[i].posthook) { char const *argv[4]; argv[0] = blog->config.templates[i].posthook; argv[1] = blog->config.templates[i].file; argv[2] = request->f.regenerate ? "regenerate" : "new"; argv[3] = NULL; run_hook("template-post-hook",argv); } } return 0; } /******************************************************************/ int pagegen_items( Blog *blog, Request *request, template__t const *template, FILE *out ) { struct btm thisday; char *tags; struct callback_data cbd; assert(blog != NULL); assert(request != NULL); assert(template != NULL); assert(out != NULL); request->f.fullurl = template->fullurl; request->f.reverse = template->reverse; thisday = blog->now; callback_init(&cbd,blog,request); cbd.template = template; if (template->reverse) BlogEntryReadXD(blog,&cbd.list,&thisday,template->items); else BlogEntryReadXU(blog,&cbd.list,&thisday,template->items); tags = tag_collect(&cbd.list,blog->config.adtag); cbd.adtag = tag_pick(tags,blog->config.adtag); free(tags); generic_main(out,&cbd); free_entries(&cbd.list); free(cbd.adtag); return 0; } /************************************************************************/ int pagegen_days( Blog *blog, Request *request, template__t const *template, FILE *out ) { struct btm thisday; size_t days; char *tags; struct callback_data cbd; bool added; assert(blog != NULL); assert(request != NULL); assert(template != NULL); assert(out != NULL); request->f.fullurl = false; request->f.reverse = true; thisday = blog->now; callback_init(&cbd,blog,request); cbd.template = template; for (days = 0 , added = false ; days < template->items ; ) { BlogEntry *entry; if (btm_cmp(&thisday,&blog->first) < 0) break; entry = BlogEntryRead(blog,&thisday); if (entry) { assert(entry->valid); ListAddTail(&cbd.list,&entry->node); added = true; } thisday.part--; if (thisday.part == 0) { thisday.part = ENTRY_MAX; btm_dec_day(&thisday); if (added) days++; added = false; } } tags = tag_collect(&cbd.list,blog->config.adtag); cbd.adtag = tag_pick(tags,blog->config.adtag); free(tags); generic_main(out,&cbd); free_entries(&cbd.list); free(cbd.adtag); return 0; } /************************************************************************/ int tumbler_page(Blog *blog,Request *request,tumbler__s *spec,int (*errorf)(Blog *,Request *,int,char const *,...)) { struct callback_data cbd; struct btm start; struct btm end; char *tags; assert(blog != NULL); assert(request != NULL); assert(spec != NULL); assert(errorf); request->f.fullurl = false; /* XXX why? */ request->f.navprev = true; request->f.navnext = true; if (spec->redirect) return (*errorf)(blog,request,HTTP_NOTIMP,"redirect on this request not implemented"); if (spec->file) return display_file(spec,blog,request,errorf); callback_init(&cbd,blog,request); assert(spec->start.year >= blog->first.year); assert(spec->start.month > 0); assert(spec->start.month < 13); assert(spec->start.day > 0); assert(spec->start.day <= max_monthday(spec->start.year,spec->start.month)); assert(spec->stop.year >= blog->first.year); assert(spec->stop.month > 0); assert(spec->stop.month < 13); assert(spec->stop.day > 0); assert(spec->stop.day <= max_monthday(spec->stop.year,spec->stop.month)); start = spec->start; end = spec->stop; if (!spec->range) { request->f.navigation = true; cbd.navunit = spec->ustart > spec->ustop ? spec->ustart : spec->ustop ; cbd.previous = calculate_previous(blog,request,start,cbd.navunit); cbd.next = calculate_next(blog,request,end,cbd.navunit); } else { if (btm_cmp(&spec->start,&spec->stop) > 0) { tumbler__s newtum = *spec; swap_endpoints(&newtum); start = newtum.start; end = newtum.stop; request->f.reverse = true; } } assert(end.day <= max_monthday(end.year,end.month)); /*--------------------------------------------------------------------- ; Okay, sanity checking here ... These should be true once we hit this ; spot of the code. ;---------------------------------------------------------------------*/ assert(start.year >= 1); assert(start.month >= 1); assert(start.month <= 12); assert(start.day >= 1); assert(start.day <= max_monthday(start.year,start.month)); assert(end.year >= 1); assert(end.month >= 1); assert(end.month <= 12); assert(end.day >= 1); assert(end.day <= max_monthday(end.year,end.month)); assert(btm_cmp(&start,&end) <= 0); /*------------------------------------------------------------------------- ; okay, resume processing ... bound against the starting time of the ; blog, and the current time. ; ; From here on out, it's pretty straight forward. read a day, if it has ; entries, add it to the list, otherwise, continue on, advancing (or ; retreading) the days as we go ... ; ; these four lines replaced 65 very confused lines of code. ;-----------------------------------------------------------*/ if (request->f.reverse) BlogEntryReadBetweenD(blog,&cbd.list,&end,&start); else BlogEntryReadBetweenU(blog,&cbd.list,&start,&end); tags = tag_collect(&cbd.list,blog->config.adtag); cbd.adtag = tag_pick(tags,blog->config.adtag); free(tags); generic_main(stdout,&cbd); free_entries(&cbd.list); free(cbd.adtag); return 0; } /******************************************************************/ .