summaryrefslogtreecommitdiff
path: root/sys/src/cmd/wikifs
diff options
context:
space:
mode:
authorTaru Karttunen <taruti@taruti.net>2011-03-30 15:46:40 +0300
committerTaru Karttunen <taruti@taruti.net>2011-03-30 15:46:40 +0300
commite5888a1ffdae813d7575f5fb02275c6bb07e5199 (patch)
treed8d51eac403f07814b9e936eed0c9a79195e2450 /sys/src/cmd/wikifs
Import sources from 2011-03-30 iso image
Diffstat (limited to 'sys/src/cmd/wikifs')
-rwxr-xr-xsys/src/cmd/wikifs/fs.c880
-rwxr-xr-xsys/src/cmd/wikifs/io.c700
-rwxr-xr-xsys/src/cmd/wikifs/lookup.c12
-rwxr-xr-xsys/src/cmd/wikifs/map.c7
-rwxr-xr-xsys/src/cmd/wikifs/mkfile33
-rwxr-xr-xsys/src/cmd/wikifs/parse.c331
-rwxr-xr-xsys/src/cmd/wikifs/parsehist.c130
-rwxr-xr-xsys/src/cmd/wikifs/testwrite.c52
-rwxr-xr-xsys/src/cmd/wikifs/tohtml.c825
-rwxr-xr-xsys/src/cmd/wikifs/util.c133
-rwxr-xr-xsys/src/cmd/wikifs/wdir.c76
-rwxr-xr-xsys/src/cmd/wikifs/wiki.h121
-rwxr-xr-xsys/src/cmd/wikifs/wiki2html.c67
-rwxr-xr-xsys/src/cmd/wikifs/wiki2text.c50
14 files changed, 3417 insertions, 0 deletions
diff --git a/sys/src/cmd/wikifs/fs.c b/sys/src/cmd/wikifs/fs.c
new file mode 100755
index 000000000..d67a2c0de
--- /dev/null
+++ b/sys/src/cmd/wikifs/fs.c
@@ -0,0 +1,880 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+#include <auth.h>
+#include <fcall.h>
+#include <9p.h>
+
+enum {
+ Qindexhtml,
+ Qindextxt,
+ Qraw,
+ Qhistoryhtml,
+ Qhistorytxt,
+ Qdiffhtml,
+ Qedithtml,
+ Qwerrorhtml,
+ Qwerrortxt,
+ Qhttplogin,
+ Nfile,
+};
+
+static char *filelist[] = {
+ "index.html",
+ "index.txt",
+ "current",
+ "history.html",
+ "history.txt",
+ "diff.html",
+ "edit.html",
+ "werror.html",
+ "werror.txt",
+ ".httplogin",
+};
+
+static int needhist[Nfile] = {
+[Qhistoryhtml] 1,
+[Qhistorytxt] 1,
+[Qdiffhtml] 1,
+};
+
+/*
+ * The qids are <8-bit type><16-bit page number><16-bit page version><8-bit file index>.
+ */
+enum { /* <8-bit type> */
+ Droot = 1,
+ D1st,
+ D2nd,
+ Fnew,
+ Fmap,
+ F1st,
+ F2nd,
+};
+
+uvlong
+mkqid(int type, int num, int vers, int file)
+{
+ return ((uvlong)type<<40) | ((uvlong)num<<24) | (vers<<8) | file;
+}
+
+int
+qidtype(uvlong path)
+{
+ return (path>>40)&0xFF;
+}
+
+int
+qidnum(uvlong path)
+{
+ return (path>>24)&0xFFFF;
+}
+
+int
+qidvers(uvlong path)
+{
+ return (path>>8)&0xFFFF;
+}
+
+int
+qidfile(uvlong path)
+{
+ return path&0xFF;
+}
+
+typedef struct Aux Aux;
+struct Aux {
+ String *name;
+ Whist *w;
+ int n;
+ ulong t;
+ String *s;
+ Map *map;
+};
+
+static void
+fsattach(Req *r)
+{
+ Aux *a;
+
+ if(r->ifcall.aname && r->ifcall.aname[0]){
+ respond(r, "invalid attach specifier");
+ return;
+ }
+
+ a = emalloc(sizeof(Aux));
+ r->fid->aux = a;
+ a->name = s_copy(r->ifcall.uname);
+
+ r->ofcall.qid = (Qid){mkqid(Droot, 0, 0, 0), 0, QTDIR};
+ r->fid->qid = r->ofcall.qid;
+ respond(r, nil);
+}
+
+static String *
+httplogin(void)
+{
+ String *s=s_new();
+ Biobuf *b;
+
+ if((b = wBopen(".httplogin", OREAD)) == nil)
+ goto Return;
+
+ while(s_read(b, s, Bsize) > 0)
+ ;
+ Bterm(b);
+
+Return:
+ return s;
+}
+
+static char*
+fswalk1(Fid *fid, char *name, Qid *qid)
+{
+ char *q;
+ int i, isdotdot, n, t;
+ uvlong path;
+ Aux *a;
+ Whist *wh;
+ String *s;
+
+ isdotdot = strcmp(name, "..")==0;
+ n = strtoul(name, &q, 10);
+ path = fid->qid.path;
+ a = fid->aux;
+
+ switch(qidtype(path)){
+ case 0:
+ return "wikifs: bad path in server (bug)";
+
+ case Droot:
+ if(isdotdot){
+ *qid = fid->qid;
+ return nil;
+ }
+ if(strcmp(name, "new")==0){
+ *qid = (Qid){mkqid(Fnew, 0, 0, 0), 0, 0};
+ return nil;
+ }
+ if(strcmp(name, "map")==0){
+ *qid = (Qid){mkqid(Fmap, 0, 0, 0), 0, 0};
+ return nil;
+ }
+ if((*q!='\0' || (wh=getcurrent(n))==nil)
+ && (wh=getcurrentbyname(name))==nil)
+ return "file does not exist";
+ *qid = (Qid){mkqid(D1st, wh->n, 0, 0), wh->doc->time, QTDIR};
+ a->w = wh;
+ return nil;
+
+ case D1st:
+ if(isdotdot){
+ *qid = (Qid){mkqid(Droot, 0, 0, 0), 0, QTDIR};
+ return nil;
+ }
+
+ /* handle history directories */
+ if(*q == '\0'){
+ if((wh = gethistory(qidnum(path))) == nil)
+ return "file does not exist";
+ for(i=0; i<wh->ndoc; i++)
+ if(wh->doc[i].time == n)
+ break;
+ if(i==wh->ndoc){
+ closewhist(wh);
+ return "file does not exist";
+ }
+ closewhist(a->w);
+ a->w = wh;
+ a->n = i;
+ *qid = (Qid){mkqid(D2nd, qidnum(path), i, 0), wh->doc[i].time, QTDIR};
+ return nil;
+ }
+
+ /* handle files other than index */
+ for(i=0; i<nelem(filelist); i++){
+ if(strcmp(name, filelist[i])==0){
+ if(needhist[i]){
+ if((wh = gethistory(qidnum(path))) == nil)
+ return "file does not exist";
+ closewhist(a->w);
+ a->w = wh;
+ }
+ *qid = (Qid){mkqid(F1st, qidnum(path), 0, i), a->w->doc->time, 0};
+ goto Gotfile;
+ }
+ }
+ return "file does not exist";
+
+ case D2nd:
+ if(isdotdot){
+ /*
+ * Can't use a->w[a->ndoc-1] because that
+ * might be a failed write rather than the real one.
+ */
+ *qid = (Qid){mkqid(D1st, qidnum(path), 0, 0), 0, QTDIR};
+ if((wh = getcurrent(qidnum(path))) == nil)
+ return "file does not exist";
+ closewhist(a->w);
+ a->w = wh;
+ a->n = 0;
+ return nil;
+ }
+ for(i=0; i<=Qraw; i++){
+ if(strcmp(name, filelist[i])==0){
+ *qid = (Qid){mkqid(F2nd, qidnum(path), qidvers(path), i), a->w->doc->time, 0};
+ goto Gotfile;
+ }
+ }
+ return "file does not exist";
+
+ default:
+ return "bad programming";
+ }
+ /* not reached */
+
+Gotfile:
+ t = qidtype(qid->path);
+ switch(qidfile(qid->path)){
+ case Qindexhtml:
+ s = tohtml(a->w, a->w->doc+a->n,
+ t==F1st? Tpage : Toldpage);
+ break;
+ case Qindextxt:
+ s = totext(a->w, a->w->doc+a->n,
+ t==F1st? Tpage : Toldpage);
+ break;
+ case Qraw:
+ s = s_copy(a->w->title);
+ s = s_append(s, "\n");
+ s = doctext(s, &a->w->doc[a->n]);
+ break;
+ case Qhistoryhtml:
+ s = tohtml(a->w, a->w->doc+a->n, Thistory);
+ break;
+ case Qhistorytxt:
+ s = totext(a->w, a->w->doc+a->n, Thistory);
+ break;
+ case Qdiffhtml:
+ s = tohtml(a->w, a->w->doc+a->n, Tdiff);
+ break;
+ case Qedithtml:
+ s = tohtml(a->w, a->w->doc+a->n, Tedit);
+ break;
+ case Qwerrorhtml:
+ s = tohtml(a->w, a->w->doc+a->n, Twerror);
+ break;
+ case Qwerrortxt:
+ s = totext(a->w, a->w->doc+a->n, Twerror);
+ break;
+ case Qhttplogin:
+ s = httplogin();
+ break;
+ default:
+ return "internal error";
+ }
+ a->s = s;
+ return nil;
+}
+
+static void
+fsopen(Req *r)
+{
+ int t;
+ uvlong path;
+ Aux *a;
+ Fid *fid;
+ Whist *wh;
+
+ fid = r->fid;
+ path = fid->qid.path;
+ t = qidtype(fid->qid.path);
+ if((r->ifcall.mode != OREAD && t != Fnew && t != Fmap)
+ || (r->ifcall.mode&ORCLOSE)){
+ respond(r, "permission denied");
+ return;
+ }
+
+ a = fid->aux;
+ switch(t){
+ case Droot:
+ currentmap(0);
+ rlock(&maplock);
+ a->map = map;
+ incref(map);
+ runlock(&maplock);
+ respond(r, nil);
+ break;
+
+ case D1st:
+ if((wh = gethistory(qidnum(path))) == nil){
+ respond(r, "file does not exist");
+ return;
+ }
+ closewhist(a->w);
+ a->w = wh;
+ a->n = a->w->ndoc-1;
+ r->ofcall.qid.vers = wh->doc[a->n].time;
+ r->fid->qid = r->ofcall.qid;
+ respond(r, nil);
+ break;
+
+ case D2nd:
+ respond(r, nil);
+ break;
+
+ case Fnew:
+ a->s = s_copy("");
+ respond(r, nil);
+ break;
+
+ case Fmap:
+ case F1st:
+ case F2nd:
+ respond(r, nil);
+ break;
+
+ default:
+ respond(r, "programmer error");
+ break;
+ }
+}
+
+static char*
+fsclone(Fid *old, Fid *new)
+{
+ Aux *a;
+
+ a = emalloc(sizeof(*a));
+ *a = *(Aux*)old->aux;
+ if(a->s)
+ s_incref(a->s);
+ if(a->w)
+ incref(a->w);
+ if(a->map)
+ incref(a->map);
+ if(a->name)
+ s_incref(a->name);
+ new->aux = a;
+ new->qid = old->qid;
+
+ return nil;
+}
+
+static void
+fsdestroyfid(Fid *fid)
+{
+ Aux *a;
+
+ a = fid->aux;
+ if(a==nil)
+ return;
+
+ if(a->name)
+ s_free(a->name);
+ if(a->map)
+ closemap(a->map);
+ if(a->s)
+ s_free(a->s);
+ if(a->w)
+ closewhist(a->w);
+ free(a);
+ fid->aux = nil;
+}
+
+static void
+fillstat(Dir *d, uvlong path, ulong tm, ulong length)
+{
+ char tmp[32], *p;
+ int type;
+
+ memset(d, 0, sizeof(Dir));
+ d->uid = estrdup9p("wiki");
+ d->gid = estrdup9p("wiki");
+
+ switch(qidtype(path)){
+ case Droot:
+ case D1st:
+ case D2nd:
+ type = QTDIR;
+ break;
+ default:
+ type = 0;
+ break;
+ }
+ d->qid = (Qid){path, tm, type};
+
+ d->atime = d->mtime = tm;
+ d->length = length;
+ if(qidfile(path) == Qedithtml)
+ d->atime = d->mtime = time(0);
+
+ switch(qidtype(path)){
+ case Droot:
+ d->name = estrdup("/");
+ d->mode = DMDIR|0555;
+ break;
+
+ case D1st:
+ d->name = numtoname(qidnum(path));
+ if(d->name == nil)
+ d->name = estrdup("<dead>");
+ for(p=d->name; *p; p++)
+ if(*p==' ')
+ *p = '_';
+ d->mode = DMDIR|0555;
+ break;
+
+ case D2nd:
+ snprint(tmp, sizeof tmp, "%lud", tm);
+ d->name = estrdup(tmp);
+ d->mode = DMDIR|0555;
+ break;
+
+ case Fmap:
+ d->name = estrdup("map");
+ d->mode = 0666;
+ break;
+
+ case Fnew:
+ d->name = estrdup("new");
+ d->mode = 0666;
+ break;
+
+ case F1st:
+ d->name = estrdup(filelist[qidfile(path)]);
+ d->mode = 0444;
+ break;
+
+ case F2nd:
+ d->name = estrdup(filelist[qidfile(path)]);
+ d->mode = 0444;
+ break;
+
+ default:
+ print("bad qid path 0x%.8llux\n", path);
+ break;
+ }
+}
+
+static void
+fsstat(Req *r)
+{
+ Aux *a;
+ Fid *fid;
+ ulong t;
+
+ t = 0;
+ fid = r->fid;
+ if((a = fid->aux) && a->w)
+ t = a->w->doc[a->n].time;
+
+ fillstat(&r->d, fid->qid.path, t, a->s ? s_len(a->s) : 0);
+ respond(r, nil);
+}
+
+typedef struct Bogus Bogus;
+struct Bogus {
+ uvlong path;
+ Aux *a;
+};
+
+static int
+rootgen(int i, Dir *d, void *aux)
+{
+ Aux *a;
+ Bogus *b;
+
+ b = aux;
+ a = b->a;
+ switch(i){
+ case 0: /* new */
+ fillstat(d, mkqid(Fnew, 0, 0, 0), a->map->t, 0);
+ return 0;
+ case 1: /* map */
+ fillstat(d, mkqid(Fmap, 0, 0, 0), a->map->t, 0);
+ return 0;
+ default: /* first-level directory */
+ i -= 2;
+ if(i >= a->map->nel)
+ return -1;
+ fillstat(d, mkqid(D1st, a->map->el[i].n, 0, 0), a->map->t, 0);
+ return 0;
+ }
+}
+
+static int
+firstgen(int i, Dir *d, void *aux)
+{
+ ulong t;
+ Bogus *b;
+ int num;
+ Aux *a;
+
+ b = aux;
+ num = qidnum(b->path);
+ a = b->a;
+ t = a->w->doc[a->n].time;
+
+ if(i < Nfile){ /* file in first-level directory */
+ fillstat(d, mkqid(F1st, num, 0, i), t, 0);
+ return 0;
+ }
+ i -= Nfile;
+
+ if(i < a->w->ndoc){ /* second-level (history) directory */
+ fillstat(d, mkqid(D2nd, num, i, 0), a->w->doc[i].time, 0);
+ return 0;
+ }
+ //i -= a->w->ndoc;
+
+ return -1;
+}
+
+static int
+secondgen(int i, Dir *d, void *aux)
+{
+ Bogus *b;
+ uvlong path;
+ Aux *a;
+
+ b = aux;
+ path = b->path;
+ a = b->a;
+
+ if(i <= Qraw){ /* index.html, index.txt, raw */
+ fillstat(d, mkqid(F2nd, qidnum(path), qidvers(path), i), a->w->doc[a->n].time, 0);
+ return 0;
+ }
+ //i -= Qraw;
+
+ return -1;
+}
+
+static void
+fsread(Req *r)
+{
+ char *t, *s;
+ uvlong path;
+ Aux *a;
+ Bogus b;
+
+ a = r->fid->aux;
+ path = r->fid->qid.path;
+ b.a = a;
+ b.path = path;
+ switch(qidtype(path)){
+ default:
+ respond(r, "cannot happen (bad qid)");
+ return;
+
+ case Droot:
+ if(a == nil || a->map == nil){
+ respond(r, "cannot happen (no map)");
+ return;
+ }
+ dirread9p(r, rootgen, &b);
+ respond(r, nil);
+ return;
+
+ case D1st:
+ if(a == nil || a->w == nil){
+ respond(r, "cannot happen (no wh)");
+ return;
+ }
+ dirread9p(r, firstgen, &b);
+ respond(r, nil);
+ return;
+
+ case D2nd:
+ dirread9p(r, secondgen, &b);
+ respond(r, nil);
+ return;
+
+ case Fnew:
+ if(a->s){
+ respond(r, "protocol botch");
+ return;
+ }
+ /* fall through */
+ case Fmap:
+ t = numtoname(a->n);
+ if(t == nil){
+ respond(r, "unknown name");
+ return;
+ }
+ for(s=t; *s; s++)
+ if(*s == ' ')
+ *s = '_';
+ readstr(r, t);
+ free(t);
+ respond(r, nil);
+ return;
+
+ case F1st:
+ case F2nd:
+ if(a == nil || a->s == nil){
+ respond(r, "cannot happen (no s)");
+ return;
+ }
+ readbuf(r, s_to_c(a->s), s_len(a->s));
+ respond(r, nil);
+ return;
+ }
+}
+
+typedef struct Sread Sread;
+struct Sread {
+ char *rp;
+};
+
+static char*
+Srdline(void *v, int c)
+{
+ char *p, *rv;
+ Sread *s;
+
+ s = v;
+ if(s->rp == nil)
+ rv = nil;
+ else if(p = strchr(s->rp, c)){
+ *p = '\0';
+ rv = s->rp;
+ s->rp = p+1;
+ }else{
+ rv = s->rp;
+ s->rp = nil;
+ }
+ return rv;
+}
+
+static void
+responderrstr(Req *r)
+{
+ char buf[ERRMAX];
+
+ rerrstr(buf, sizeof buf);
+ if(buf[0] == '\0')
+ strcpy(buf, "unknown error");
+ respond(r, buf);
+}
+
+static void
+fswrite(Req *r)
+{
+ char *author, *comment, *net, *err, *p, *title, tmp[40];
+ int rv, n;
+ ulong t;
+ Aux *a;
+ Fid *fid;
+ Sread s;
+ String *stmp;
+ Whist *w;
+
+ fid = r->fid;
+ a = fid->aux;
+ switch(qidtype(fid->qid.path)){
+ case Fmap:
+ stmp = s_nappend(s_reset(nil), r->ifcall.data, r->ifcall.count);
+ a->n = nametonum(s_to_c(stmp));
+ s_free(stmp);
+ if(a->n < 0)
+ respond(r, "name not found");
+ else
+ respond(r, nil);
+ return;
+ case Fnew:
+ break;
+ default:
+ respond(r, "cannot happen");
+ return;
+ }
+
+ if(a->s == nil){
+ respond(r, "protocol botch");
+ return;
+ }
+ if(r->ifcall.count==0){ /* do final processing */
+ s.rp = s_to_c(a->s);
+ w = nil;
+ err = "bad format";
+ if((title = Srdline(&s, '\n')) == nil){
+ Error:
+ if(w)
+ closewhist(w);
+ s_free(a->s);
+ a->s = nil;
+ respond(r, err);
+ return;
+ }
+
+ w = emalloc(sizeof(*w));
+ incref(w);
+ w->title = estrdup(title);
+
+ t = 0;
+ author = estrdup(s_to_c(a->name));
+
+ comment = nil;
+ while(s.rp && *s.rp && *s.rp != '\n'){
+ p = Srdline(&s, '\n');
+ assert(p != nil);
+ switch(p[0]){
+ case 'A':
+ free(author);
+ author = estrdup(p+1);
+ break;
+ case 'D':
+ t = strtoul(p+1, &p, 10);
+ if(*p != '\0')
+ goto Error;
+ break;
+ case 'C':
+ free(comment);
+ comment = estrdup(p+1);
+ break;
+ }
+ }
+
+ w->doc = emalloc(sizeof(w->doc[0]));
+ w->doc->time = time(0);
+ w->doc->comment = comment;
+
+ if(net = r->pool->srv->aux){
+ p = emalloc(strlen(author)+10+strlen(net));
+ strcpy(p, author);
+ strcat(p, " (");
+ strcat(p, net);
+ strcat(p, ")");
+ free(author);
+ author = p;
+ }
+ w->doc->author = author;
+
+ if((w->doc->wtxt = Brdpage(Srdline, &s)) == nil){
+ err = "empty document";
+ goto Error;
+ }
+
+ w->ndoc = 1;
+ if((n = allocnum(w->title, 0)) < 0)
+ goto Error;
+ sprint(tmp, "D%lud\n", w->doc->time);
+ a->s = s_reset(a->s);
+ a->s = doctext(a->s, w->doc);
+ rv = writepage(n, t, a->s, w->title);
+ s_free(a->s);
+ a->s = nil;
+ a->n = n;
+ closewhist(w);
+ if(rv < 0)
+ responderrstr(r);
+ else
+ respond(r, nil);
+ return;
+ }
+
+ if(s_len(a->s)+r->ifcall.count > Maxfile){
+ respond(r, "file too large");
+ s_free(a->s);
+ a->s = nil;
+ return;
+ }
+ a->s = s_nappend(a->s, r->ifcall.data, r->ifcall.count);
+ r->ofcall.count = r->ifcall.count;
+ respond(r, nil);
+}
+
+Srv wikisrv = {
+.attach= fsattach,
+.destroyfid= fsdestroyfid,
+.clone= fsclone,
+.walk1= fswalk1,
+.open= fsopen,
+.read= fsread,
+.write= fswrite,
+.stat= fsstat,
+};
+
+void
+usage(void)
+{
+ fprint(2, "usage: wikifs [-D] [-a addr]... [-m mtpt] [-p perm] [-s service] dir\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ char **addr;
+ int i, naddr;
+ char *buf;
+ char *service, *mtpt;
+ ulong perm;
+ Dir d, *dp;
+ Srv *s;
+
+ naddr = 0;
+ addr = nil;
+ perm = 0;
+ service = nil;
+ mtpt = "/mnt/wiki";
+ ARGBEGIN{
+ case 'D':
+ chatty9p++;
+ break;
+ case 'a':
+ if(naddr%8 == 0)
+ addr = erealloc(addr, (naddr+8)*sizeof(addr[0]));
+ addr[naddr++] = EARGF(usage());
+ break;
+ case 'm':
+ mtpt = EARGF(usage());
+ break;
+ case 'M':
+ mtpt = nil;
+ break;
+ case 'p':
+ perm = strtoul(EARGF(usage()), nil, 8);
+ break;
+ case 's':
+ service = EARGF(usage());
+ break;
+ default:
+ usage();
+ break;
+ }ARGEND
+
+ if(argc != 1)
+ usage();
+
+ if((dp = dirstat(argv[0])) == nil)
+ sysfatal("dirstat %s: %r", argv[0]);
+ if((dp->mode&DMDIR) == 0)
+ sysfatal("%s: not a directory", argv[0]);
+ free(dp);
+ wikidir = argv[0];
+
+ currentmap(0);
+
+ for(i=0; i<naddr; i++)
+ listensrv(&wikisrv, addr[i]);
+
+ s = emalloc(sizeof *s);
+ *s = wikisrv;
+ postmountsrv(s, service, mtpt, MREPL|MCREATE);
+ if(perm){
+ buf = emalloc9p(5+strlen(service)+1);
+ strcpy(buf, "/srv/");
+ strcat(buf, service);
+ nulldir(&d);
+ d.mode = perm;
+ if(dirwstat(buf, &d) < 0)
+ fprint(2, "wstat: %r\n");
+ free(buf);
+ }
+ exits(nil);
+}
diff --git a/sys/src/cmd/wikifs/io.c b/sys/src/cmd/wikifs/io.c
new file mode 100755
index 000000000..43743744e
--- /dev/null
+++ b/sys/src/cmd/wikifs/io.c
@@ -0,0 +1,700 @@
+/*
+ * I/O for a Wiki document set.
+ *
+ * The files are kept in one flat directory.
+ * There are three files for each document:
+ * nnn - current version of the document
+ * nnn.hist - history (all old versions) of the document
+ * append-only
+ * L.nnn - write lock file for the document
+ *
+ * At the moment, since we don't have read/write locks
+ * in the file system, we use the L.nnn file as a read lock too.
+ * It's a hack but there aren't supposed to be many readers
+ * anyway.
+ *
+ * The nnn.hist file is in the format read by Brdwhist.
+ * The nnn file is in that format too, but only contains the
+ * last entry of the nnn.hist file.
+ *
+ * In addition to this set of files, there is an append-only
+ * map file that provides a mapping between numbers and titles.
+ * The map file is a sequence of lines of the form
+ * nnn Title Here
+ * The lock file L.map must be held to add to the end, to
+ * make sure that the numbers are allocated sequentially.
+ *
+ * We assume that writes to the map file will fit in one message,
+ * so that we don't have to read-lock the file.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+enum {
+ Nhash = 64,
+ Mcache = 128,
+};
+
+typedef struct Wcache Wcache;
+struct Wcache {
+ int n;
+ ulong use;
+ RWLock;
+ ulong tcurrent;
+ ulong thist;
+ Whist *hist;
+ Whist *current;
+ Qid qid;
+ Qid qidhist;
+ Wcache *hash;
+};
+
+static RWLock cachelock;
+static Wcache *tab[Nhash];
+int ncache;
+
+void
+closewhist(Whist *wh)
+{
+ int i;
+
+ if(wh && decref(wh) == 0){
+ free(wh->title);
+ for(i=0; i<wh->ndoc; i++){
+ free(wh->doc[i].author);
+ free(wh->doc[i].comment);
+ freepage(wh->doc[i].wtxt);
+ }
+ free(wh->doc);
+ free(wh);
+ }
+}
+
+void
+freepage(Wpage *p)
+{
+ Wpage *next;
+
+ for(; p; p=next){
+ next = p->next;
+ free(p->text);
+ free(p->url);
+ free(p);
+ }
+}
+
+static Wcache*
+findcache(int n)
+{
+ Wcache *w;
+
+ for(w=tab[n%Nhash]; w; w=w->hash)
+ if(w->n == n){
+ w->use = time(0);
+ return w;
+ }
+ return nil;
+}
+
+static int
+getlock(char *lock)
+{
+ char buf[ERRMAX];
+ int i, fd;
+ enum { SECS = 200 };
+
+ for(i=0; i<SECS*10; i++){
+ fd = wcreate(lock, ORDWR, DMEXCL|0666);
+ if(fd >= 0)
+ return fd;
+ buf[0] = '\0';
+ rerrstr(buf, sizeof buf);
+ if(strstr(buf, "locked") == nil)
+ break;
+ sleep(1000/10);
+ }
+ werrstr("couldn't acquire lock %s: %r", lock);
+ return -1;
+}
+
+static Whist*
+readwhist(char *file, char *lock, Qid *qid)
+{
+ int lfd;
+ Biobuf *b;
+ Dir *d;
+ Whist *wh;
+
+ if((lfd=getlock(lock)) < 0) // LOG?
+ return nil;
+
+ if(qid){
+ if((d = wdirstat(file)) == nil){
+ close(lfd);
+ return nil;
+ }
+ *qid = d->qid;
+ free(d);
+ }
+
+ if((b = wBopen(file, OREAD)) == nil){ //LOG?
+ close(lfd);
+ return nil;
+ }
+
+ wh = Brdwhist(b);
+
+ Bterm(b);
+ close(lfd);
+ return wh;
+}
+
+static void
+gencurrent(Wcache *w, Qid *q, char *file, char *lock, ulong *t, Whist **wp, int n)
+{
+ Dir *d;
+ Whist *wh;
+
+ if(*wp && *t+Tcache >= time(0))
+ return;
+
+ wlock(w);
+ if(*wp && *t+Tcache >= time(0)){
+ wunlock(w);
+ return;
+ }
+
+ if(((d = wdirstat(file)) == nil) || (d->qid.path==q->path && d->qid.vers==q->vers)){
+ *t = time(0);
+ wunlock(w);
+ free(d);
+ return;
+ }
+
+ free(d);
+ if(wh = readwhist(file, lock, q)){
+ wh->n = n;
+ *t = time(0);
+ closewhist(*wp);
+ *wp = wh;
+ }
+else fprint(2, "error file=%s lock=%s %r\n", file, lock);
+ wunlock(w);
+}
+
+static void
+current(Wcache *w)
+{
+ char tmp[40];
+ char tmplock[40];
+
+ sprint(tmplock, "d/L.%d", w->n);
+ sprint(tmp, "d/%d", w->n);
+ gencurrent(w, &w->qid, tmp, tmplock, &w->tcurrent, &w->current, w->n);
+}
+
+static void
+currenthist(Wcache *w)
+{
+ char hist[40], lock[40];
+
+ sprint(hist, "d/%d.hist", w->n);
+ sprint(lock, "d/L.%d", w->n);
+
+ gencurrent(w, &w->qidhist, hist, lock, &w->thist, &w->hist, w->n);
+}
+
+void
+voidcache(int n)
+{
+ Wcache *c;
+
+ rlock(&cachelock);
+ if(c = findcache(n)){
+ wlock(c);
+ c->tcurrent = 0;
+ c->thist = 0;
+ /* aggressively free memory */
+ closewhist(c->hist);
+ c->hist = nil;
+ closewhist(c->current);
+ c->current = nil;
+ wunlock(c);
+ }
+ runlock(&cachelock);
+}
+
+static Whist*
+getcache(int n, int hist)
+{
+ int i, isw;
+ ulong t;
+ Wcache *c, **cp, **evict;
+ Whist *wh;
+
+ isw = 0;
+ rlock(&cachelock);
+ if(c = findcache(n)){
+ Found:
+ current(c);
+ if(hist)
+ currenthist(c);
+ rlock(c);
+ if(hist)
+ wh = c->hist;
+ else
+ wh = c->current;
+ if(wh)
+ incref(wh);
+ runlock(c);
+ if(isw)
+ wunlock(&cachelock);
+ else
+ runlock(&cachelock);
+ return wh;
+ }
+ runlock(&cachelock);
+
+ wlock(&cachelock);
+ if(c = findcache(n)){
+ isw = 1; /* better to downgrade lock but can't */
+ goto Found;
+ }
+
+ if(ncache < Mcache){
+ Alloc:
+ c = emalloc(sizeof *c);
+ ncache++;
+ }else{
+ /* find something to evict. */
+ t = ~0;
+ evict = nil;
+ for(i=0; i<Nhash; i++){
+ for(cp=&tab[i], c=*cp; c; cp=&c->hash, c=*cp){
+ if(c->use < t
+ && (!c->hist || c->hist->ref==1)
+ && (!c->current || c->current->ref==1)){
+ evict = cp;
+ t = c->use;
+ }
+ }
+ }
+
+ if(evict == nil){
+ fprint(2, "wikifs: nothing to evict\n");
+ goto Alloc;
+ }
+
+ c = *evict;
+ *evict = c->hash;
+
+ closewhist(c->current);
+ closewhist(c->hist);
+ memset(c, 0, sizeof *c);
+ }
+
+ c->n = n;
+ c->hash = tab[n%Nhash];
+ tab[n%Nhash] = c;
+ isw = 1;
+ goto Found;
+}
+
+Whist*
+getcurrent(int n)
+{
+ return getcache(n, 0);
+}
+
+Whist*
+gethistory(int n)
+{
+ return getcache(n, 1);
+}
+
+RWLock maplock;
+Map *map;
+
+static int
+mapcmp(const void *va, const void *vb)
+{
+ Mapel *a, *b;
+
+ a = (Mapel*)va;
+ b = (Mapel*)vb;
+
+ return strcmp(a->s, b->s);
+}
+
+void
+closemap(Map *m)
+{
+ if(decref(m)==0){
+ free(m->buf);
+ free(m->el);
+ free(m);
+ }
+}
+
+void
+currentmap(int force)
+{
+ char *p, *q, *r;
+ int lfd, fd, m, n;
+ Dir *d;
+ Map *nmap;
+ char *err = nil;
+
+ lfd = -1;
+ fd = -1;
+ d = nil;
+ nmap = nil;
+ if(!force && map && map->t+Tcache >= time(0))
+ return;
+
+ wlock(&maplock);
+ if(!force && map && map->t+Tcache >= time(0))
+ goto Return;
+
+ if((lfd = getlock("d/L.map")) < 0){
+ err = "can't lock";
+ goto Return;
+ }
+
+ if((d = wdirstat("d/map")) == nil)
+ goto Return;
+
+ if(map && d->qid.path == map->qid.path && d->qid.vers == map->qid.vers){
+ map->t = time(0);
+ goto Return;
+ }
+
+ if(d->length > Maxmap){
+ //LOG
+ err = "too long";
+ goto Return;
+ }
+
+ if((fd = wopen("d/map", OREAD)) < 0)
+ goto Return;
+
+ nmap = emalloc(sizeof *nmap);
+ nmap->buf = emalloc(d->length+1);
+ n = readn(fd, nmap->buf, d->length);
+ if(n != d->length){
+ err = "bad length";
+ goto Return;
+ }
+ nmap->buf[n] = '\0';
+
+ n = 0;
+ for(p=nmap->buf; p; p=strchr(p+1, '\n'))
+ n++;
+ nmap->el = emalloc(n*sizeof(nmap->el[0]));
+
+ m = 0;
+ for(p=nmap->buf; p && *p && m < n; p=q){
+ if(q = strchr(p+1, '\n'))
+ *q++ = '\0';
+ nmap->el[m].n = strtol(p, &r, 10);
+ if(*r == ' ')
+ r++;
+ else
+ {}//LOG?
+ nmap->el[m].s = strcondense(r, 1);
+ m++;
+ }
+ //LOG if m != n
+
+ nmap->qid = d->qid;
+ nmap->t = time(0);
+ nmap->nel = m;
+ qsort(nmap->el, nmap->nel, sizeof(nmap->el[0]), mapcmp);
+ if(map)
+ closemap(map);
+ map = nmap;
+ incref(map);
+ nmap = nil;
+
+Return:
+ free(d);
+ if(nmap){
+ free(nmap->el);
+ free(nmap->buf);
+ free(nmap);
+ }
+ if(map == nil)
+ sysfatal("cannot get map: %s: %r", err);
+
+ if(fd >= 0)
+ close(fd);
+ if(lfd >= 0)
+ close(lfd);
+ wunlock(&maplock);
+}
+
+int
+allocnum(char *title, int mustbenew)
+{
+ char *p, *q;
+ int lfd, fd, n;
+ Biobuf b;
+
+ if(strcmp(title, "map")==0 || strcmp(title, "new")==0){
+ werrstr("reserved title name");
+ return -1;
+ }
+
+ if(title[0]=='\0' || strpbrk(title, "/<>:?")){
+ werrstr("invalid character in name");
+ return -1;
+ }
+ if((n = nametonum(title)) >= 0){
+ if(mustbenew){
+ werrstr("duplicate title");
+ return -1;
+ }
+ return n;
+ }
+
+ title = estrdup(title);
+ strcondense(title, 1);
+ strlower(title);
+ if(strchr(title, '\n') || strlen(title) > 200){
+ werrstr("bad title");
+ free(title);
+ return -1;
+ }
+
+ if((lfd = getlock("d/L.map")) < 0){
+ free(title);
+ return -1;
+ }
+
+ if((fd = wopen("d/map", ORDWR)) < 0){ // LOG?
+ close(lfd);
+ free(title);
+ return -1;
+ }
+
+ /*
+ * What we really need to do here is make sure the
+ * map is up-to-date, then make sure the title isn't
+ * taken, and then add it, all without dropping the locks.
+ *
+ * This turns out to be a mess when you start adding
+ * all the necessary dolock flags, so instead we just
+ * read through the file ourselves, and let our
+ * map catch up on its own.
+ */
+ Binit(&b, fd, OREAD);
+ n = 0;
+ while(p = Brdline(&b, '\n')){
+ p[Blinelen(&b)-1] = '\0';
+ n = atoi(p)+1;
+ q = strchr(p, ' ');
+ if(q == nil)
+ continue;
+ if(strcmp(q+1, title) == 0){
+ free(title);
+ close(fd);
+ close(lfd);
+ if(mustbenew){
+ werrstr("duplicate title");
+ return -1;
+ }else
+ return n;
+ }
+ }
+
+ seek(fd, 0, 2); /* just in case it's not append only */
+ fprint(fd, "%d %s\n", n, title);
+ close(fd);
+ close(lfd);
+ free(title);
+ /* kick the map */
+ currentmap(1);
+ return n;
+}
+
+int
+nametonum(char *s)
+{
+ char *p;
+ int i, lo, hi, m, rv;
+
+ s = estrdup(s);
+ strlower(s);
+ for(p=s; *p; p++)
+ if(*p=='_')
+ *p = ' ';
+
+ currentmap(0);
+ rlock(&maplock);
+ lo = 0;
+ hi = map->nel;
+ while(hi-lo > 1){
+ m = (lo+hi)/2;
+ i = strcmp(s, map->el[m].s);
+ if(i < 0)
+ hi = m;
+ else
+ lo = m;
+ }
+ if(hi-lo == 1 && strcmp(s, map->el[lo].s)==0)
+ rv = map->el[lo].n;
+ else
+ rv = -1;
+ runlock(&maplock);
+ free(s);
+ return rv;
+}
+
+char*
+numtoname(int n)
+{
+ int i;
+ char *s;
+
+ currentmap(0);
+ rlock(&maplock);
+ for(i=0; i<map->nel; i++){
+ if(map->el[i].n==n)
+ break;
+ }
+ if(i==map->nel){
+ runlock(&maplock);
+ return nil;
+ }
+ s = estrdup(map->el[i].s);
+ runlock(&maplock);
+ return s;
+}
+
+Whist*
+getcurrentbyname(char *s)
+{
+ int n;
+
+ if((n = nametonum(s)) < 0)
+ return nil;
+ return getcache(n, 0);
+}
+
+static String*
+Brdstring(Biobuf *b)
+{
+ long len;
+ String *s;
+ Dir *d;
+
+ d = dirfstat(Bfildes(b));
+ if (d == nil) /* shouldn't happen, we just opened it */
+ len = 0;
+ else
+ len = d->length;
+ free(d);
+ s = s_newalloc(len);
+ s_read(b, s, len);
+ return s;
+}
+
+/*
+ * Attempt to install a new page. If t==0 we are creating.
+ * Otherwise, we are editing and t must be set to the current
+ * version (t is the version we started with) to avoid conflicting
+ * writes.
+ *
+ * If there is a conflicting write, we still write the page to
+ * the history file, but mark it as a failed write.
+ */
+int
+writepage(int num, ulong t, String *s, char *title)
+{
+ char tmp[40], tmplock[40], err[ERRMAX], hist[40], *p;
+ int conflict, lfd, fd;
+ Biobuf *b;
+ String *os;
+
+ sprint(tmp, "d/%d", num);
+ sprint(tmplock, "d/L.%d", num);
+ sprint(hist, "d/%d.hist", num);
+ if((lfd = getlock(tmplock)) < 0)
+ return -1;
+
+ conflict = 0;
+ if(b = wBopen(tmp, OREAD)){
+ Brdline(b, '\n'); /* title */
+ if(p = Brdline(b, '\n')) /* version */
+ p[Blinelen(b)-1] = '\0';
+ if(p==nil || p[0] != 'D'){
+ snprint(err, sizeof err, "bad format in extant file");
+ conflict = 1;
+ }else if(strtoul(p+1, 0, 0) != t){
+ os = Brdstring(b); /* why read the whole file? */
+ p = strchr(s_to_c(s), '\n');
+ if(p!=nil && strcmp(p+1, s_to_c(os))==0){ /* ignore dup write */
+ close(lfd);
+ s_free(os);
+ Bterm(b);
+ return 0;
+ }
+ s_free(os);
+ snprint(err, sizeof err, "update conflict %lud != %s", t, p+1);
+ conflict = 1;
+ }
+ Bterm(b);
+ }else{
+ if(t != 0){
+ close(lfd);
+ werrstr("did not expect to create");
+ return -1;
+ }
+ }
+
+ if((fd = wopen(hist, OWRITE)) < 0){
+ if((fd = wcreate(hist, OWRITE, 0666)) < 0){
+ close(lfd);
+ return -1;
+ }else
+ fprint(fd, "%s\n", title);
+ }
+ if(seek(fd, 0, 2) < 0
+ || (conflict && write(fd, "X\n", 2) != 2)
+ || write(fd, s_to_c(s), s_len(s)) != s_len(s)){
+ close(fd);
+ close(lfd);
+ return -1;
+ }
+ close(fd);
+
+ if(conflict){
+ close(lfd);
+ voidcache(num);
+ werrstr(err);
+ return -1;
+ }
+
+ if((fd = wcreate(tmp, OWRITE, 0666)) < 0){
+ close(lfd);
+ voidcache(num);
+ return -1;
+ }
+ if(write(fd, title, strlen(title)) != strlen(title)
+ || write(fd, "\n", 1) != 1
+ || write(fd, s_to_c(s), s_len(s)) != s_len(s)){
+ close(fd);
+ close(lfd);
+ voidcache(num);
+ return -1;
+ }
+ close(fd);
+ close(lfd);
+ voidcache(num);
+ return 0;
+}
diff --git a/sys/src/cmd/wikifs/lookup.c b/sys/src/cmd/wikifs/lookup.c
new file mode 100755
index 000000000..997766e92
--- /dev/null
+++ b/sys/src/cmd/wikifs/lookup.c
@@ -0,0 +1,12 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+void
+main(int argc, char **argv)
+{
+ print("%d\n", nametonum(argv[1]));
+}
diff --git a/sys/src/cmd/wikifs/map.c b/sys/src/cmd/wikifs/map.c
new file mode 100755
index 000000000..88c5953bc
--- /dev/null
+++ b/sys/src/cmd/wikifs/map.c
@@ -0,0 +1,7 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
diff --git a/sys/src/cmd/wikifs/mkfile b/sys/src/cmd/wikifs/mkfile
new file mode 100755
index 000000000..ed0d5ef2f
--- /dev/null
+++ b/sys/src/cmd/wikifs/mkfile
@@ -0,0 +1,33 @@
+</$objtype/mkfile
+
+TARG=wikifs
+
+HFILES=wiki.h
+COFILES=\
+ io.$O\
+ parse.$O\
+ parsehist.$O\
+ tohtml.$O\
+ util.$O\
+ wdir.$O\
+
+OFILES=fs.$O $COFILES
+
+BIN=/$objtype/bin
+LIB=/$objtype/lib/lib9p.a #/$objtype/lib/libdebugmalloc.a
+
+UPDATE=\
+ mkfile\
+ $HFILES\
+ ${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+$O.wiki2html: wiki2html.$O $COFILES
+ $LD -o $target $prereq
+
+$O.wiki2text: wiki2text.$O $COFILES
+ $LD -o $target $prereq
+
+$O.owikifs: ofs.$O $COFILES
+ $LD -o $target $prereq
diff --git a/sys/src/cmd/wikifs/parse.c b/sys/src/cmd/wikifs/parse.c
new file mode 100755
index 000000000..dc8924317
--- /dev/null
+++ b/sys/src/cmd/wikifs/parse.c
@@ -0,0 +1,331 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <ctype.h>
+#include <thread.h>
+#include "wiki.h"
+
+static Wpage*
+mkwtxt(int type, char *text)
+{
+ Wpage *w;
+
+ w = emalloc(sizeof(*w));
+ w->type = type;
+ w->text = text;
+ setmalloctag(w, getcallerpc(&type));
+ return w;
+}
+
+/*
+ * turn runs of whitespace into single spaces,
+ * eliminate whitespace at beginning and end.
+ */
+char*
+strcondense(char *s, int cutbegin)
+{
+ char *r, *w, *es;
+ int inspace;
+
+ es = s+strlen(s);
+ inspace = cutbegin;
+ for(r=w=s; *r; r++){
+ if(isspace(*r)){
+ if(!inspace){
+ inspace=1;
+ *w++ = ' ';
+ }
+ }else{
+ inspace=0;
+ *w++ = *r;
+ }
+ }
+ assert(w <= es);
+ if(inspace && w>s){
+ --w;
+ *w = '\0';
+ }
+ else
+ *w = '\0';
+ return s;
+}
+
+/*
+ * turn runs of Wplain into single Wplain.
+ */
+static Wpage*
+wcondense(Wpage *wtxt)
+{
+ Wpage *ow, *w;
+
+ for(w=wtxt; w; ){
+ if(w->type == Wplain)
+ strcondense(w->text, 1);
+
+ if(w->type != Wplain || w->next==nil
+ || w->next->type != Wplain){
+ w=w->next;
+ continue;
+ }
+
+ w->text = erealloc(w->text, strlen(w->text)+1+strlen(w->next->text)+1);
+ strcat(w->text, " ");
+ strcat(w->text, w->next->text);
+
+ ow = w->next;
+ w->next = ow->next;
+ ow->next = nil;
+ freepage(ow);
+ }
+ return wtxt;
+}
+
+/*
+ * Parse a link, without the brackets.
+ */
+static Wpage*
+mklink(char *s)
+{
+ char *q;
+ Wpage *w;
+
+ for(q=s; *q && *q != '|'; q++)
+ ;
+
+ if(*q == '\0'){
+ w = mkwtxt(Wlink, estrdup(strcondense(s, 1)));
+ w->url = nil;
+ }else{
+ *q = '\0';
+ w = mkwtxt(Wlink, estrdup(strcondense(s, 1)));
+ w->url = estrdup(strcondense(q+1, 1));
+ }
+ setmalloctag(w, getcallerpc(&s));
+ return w;
+}
+
+/*
+ * Parse Wplains, inserting Wlink nodes where appropriate.
+ */
+static Wpage*
+wlink(Wpage *wtxt)
+{
+ char *p, *q, *r, *s;
+ Wpage *w, *nw;
+
+ for(w=wtxt; w; w=nw){
+ nw = w->next;
+ if(w->type != Wplain)
+ continue;
+ while(w->text[0]){
+ p = w->text;
+ for(q=p; *q && *q != '['; q++)
+ ;
+ if(*q == '\0')
+ break;
+ for(r=q; *r && *r != ']'; r++)
+ ;
+ if(*r == '\0')
+ break;
+ *q = '\0';
+ *r = '\0';
+ s = w->text;
+ w->text = estrdup(w->text);
+ w->next = mklink(q+1);
+ w = w->next;
+ w->next = mkwtxt(Wplain, estrdup(r+1));
+ free(s);
+ w = w->next;
+ w->next = nw;
+ }
+ assert(w->next == nw);
+ }
+ return wtxt;
+}
+
+static int
+ismanchar(int c)
+{
+ return ('a' <= c && c <= 'z')
+ || ('A' <= c && c <= 'Z')
+ || ('0' <= c && c <= '9')
+ || c=='_' || c=='-' || c=='.' || c=='/'
+ || (c < 0); /* UTF */
+}
+
+static Wpage*
+findmanref(char *p, char **beginp, char **endp)
+{
+ char *q, *r;
+ Wpage *w;
+
+ q=p;
+ for(;;){
+ for(; q[0] && (q[0] != '(' || !isdigit(q[1]) || q[2] != ')'); q++)
+ ;
+ if(*q == '\0')
+ break;
+ for(r=q; r>p && ismanchar(r[-1]); r--)
+ ;
+ if(r==q){
+ q += 3;
+ continue;
+ }
+ *q = '\0';
+ w = mkwtxt(Wman, estrdup(r));
+ *beginp = r;
+ *q = '(';
+ w->section = q[1]-'0';
+ *endp = q+3;
+ setmalloctag(w, getcallerpc(&p));
+ return w;
+ }
+ return nil;
+}
+
+/*
+ * Parse Wplains, looking for man page references.
+ * This should be done by using a plumb(6)-style
+ * control file rather than hard-coding things here.
+ */
+static Wpage*
+wman(Wpage *wtxt)
+{
+ char *q, *r;
+ Wpage *w, *mw, *nw;
+
+ for(w=wtxt; w; w=nw){
+ nw = w->next;
+ if(w->type != Wplain)
+ continue;
+ while(w->text[0]){
+ if((mw = findmanref(w->text, &q, &r)) == nil)
+ break;
+ *q = '\0';
+ w->next = mw;
+ w = w->next;
+ w->next = mkwtxt(Wplain, estrdup(r));
+ w = w->next;
+ w->next = nw;
+ }
+ assert(w->next == nw);
+ }
+ return wtxt;
+}
+
+static int isheading(char *p) {
+ Rune r;
+ int hasupper=0;
+ while(*p) {
+ p+=chartorune(&r,p);
+ if(isupperrune(r))
+ hasupper=1;
+ else if(islowerrune(r))
+ return 0;
+ }
+ return hasupper;
+}
+
+Wpage*
+Brdpage(char *(*rdline)(void*,int), void *b)
+{
+ char *p, *c;
+ int waspara;
+ Wpage *w, **pw;
+
+ w = nil;
+ pw = &w;
+ waspara = 1;
+ while((p = rdline(b, '\n')) != nil){
+ if(p[0] != '!')
+ p = strcondense(p, 1);
+ if(p[0] == '\0'){
+ if(waspara==0){
+ waspara=1;
+ *pw = mkwtxt(Wpara, nil);
+ pw = &(*pw)->next;
+ }
+ continue;
+ }
+ waspara = 0;
+ switch(p[0]){
+ case '*':
+ *pw = mkwtxt(Wbullet, nil);
+ pw = &(*pw)->next;
+ *pw = mkwtxt(Wplain, estrdup(p+1));
+ pw = &(*pw)->next;
+ break;
+ case '!':
+ *pw = mkwtxt(Wpre, estrdup(p[1]==' '?p+2:p+1));
+ pw = &(*pw)->next;
+ break;
+ case '-':
+ for(c = p; *c != '\0'; c++) {
+ if(*c != '-') {
+ c = p;
+ break;
+ }
+ }
+
+ if( (c-p) > 4) {
+ *pw = mkwtxt(Whr, nil);
+ pw = &(*pw)->next;
+ break;
+ }
+ /* else fall thru */
+ default:
+ if(isheading(p)){
+ *pw = mkwtxt(Wheading, estrdup(p));
+ pw = &(*pw)->next;
+ continue;
+ }
+ *pw = mkwtxt(Wplain, estrdup(p));
+ pw = &(*pw)->next;
+ break;
+ }
+ }
+ if(w == nil)
+ werrstr("empty page");
+
+ *pw = nil;
+ w = wcondense(w);
+ w = wlink(w);
+ w = wman(w);
+ setmalloctag(w, getcallerpc(&rdline));
+
+ return w;
+}
+
+void
+printpage(Wpage *w)
+{
+ for(; w; w=w->next){
+ switch(w->type){
+ case Wpara:
+ print("para\n");
+ break;
+ case Wheading:
+ print("heading '%s'\n", w->text);
+ break;
+ case Wbullet:
+ print("bullet\n");
+ break;
+ case Wlink:
+ print("link '%s' '%s'\n", w->text, w->url);
+ break;
+ case Wman:
+ print("man %d %s\n", w->section, w->text);
+ break;
+ case Wplain:
+ print("plain '%s'\n", w->text);
+ break;
+ case Whr:
+ print("hr\n");
+ break;
+ case Wpre:
+ print("pre '%s'\n", w->text);
+ break;
+ }
+ }
+}
diff --git a/sys/src/cmd/wikifs/parsehist.c b/sys/src/cmd/wikifs/parsehist.c
new file mode 100755
index 000000000..88b49dd7e
--- /dev/null
+++ b/sys/src/cmd/wikifs/parsehist.c
@@ -0,0 +1,130 @@
+/*
+ * Read a Wiki history file.
+ * It's a title line then a sequence of Wiki files separated by headers.
+ *
+ * Ddate/time
+ * #body
+ * #...
+ * #...
+ * #...
+ * etc.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+static char*
+Brdwline(void *vb, int sep)
+{
+ Biobufhdr *b;
+ char *p;
+
+ b = vb;
+ if(Bgetc(b) == '#'){
+ if(p = Brdline(b, sep))
+ p[Blinelen(b)-1] = '\0';
+ return p;
+ }else{
+ Bungetc(b);
+ return nil;
+ }
+}
+
+Whist*
+Brdwhist(Biobuf *b)
+{
+ int i, current, conflict, c, n;
+ char *author, *comment, *p, *title;
+ ulong t;
+ Wdoc *w;
+ Whist *h;
+
+ if((p = Brdline(b, '\n')) == nil){
+ werrstr("short read: %r");
+ return nil;
+ }
+
+ p[Blinelen(b)-1] = '\0';
+ p = strcondense(p, 1);
+ title = estrdup(p);
+
+ w = nil;
+ n = 0;
+ t = -1;
+ author = nil;
+ comment = nil;
+ conflict = 0;
+ current = 0;
+ while((c = Bgetc(b)) != Beof){
+ if(c != '#'){
+ p = Brdline(b, '\n');
+ if(p == nil)
+ break;
+ p[Blinelen(b)-1] = '\0';
+
+ switch(c){
+ case 'D':
+ t = strtoul(p, 0, 10);
+ break;
+ case 'A':
+ free(author);
+ author = estrdup(p);
+ break;
+ case 'C':
+ free(comment);
+ comment = estrdup(p);
+ break;
+ case 'X':
+ conflict = 1;
+ }
+ } else { /* c=='#' */
+ Bungetc(b);
+ if(n%8 == 0)
+ w = erealloc(w, (n+8)*sizeof(w[0]));
+ w[n].time = t;
+ w[n].author = author;
+ w[n].comment = comment;
+ comment = nil;
+ author = nil;
+ w[n].wtxt = Brdpage(Brdwline, b);
+ w[n].conflict = conflict;
+ if(w[n].wtxt == nil)
+ goto Error;
+ if(!conflict)
+ current = n;
+ n++;
+ conflict = 0;
+ t = -1;
+ }
+ }
+ if(w==nil)
+ goto Error;
+
+ free(comment);
+ free(author);
+ h = emalloc(sizeof *h);
+ h->title = title;
+ h->doc = w;
+ h->ndoc = n;
+ h->current = current;
+ incref(h);
+ setmalloctag(h, getcallerpc(&b));
+ return h;
+
+Error:
+ free(title);
+ free(author);
+ free(comment);
+ for(i=0; i<n; i++){
+ free(w[i].author);
+ free(w[i].comment);
+ freepage(w[i].wtxt);
+ }
+ free(w);
+ return nil;
+}
+
diff --git a/sys/src/cmd/wikifs/testwrite.c b/sys/src/cmd/wikifs/testwrite.c
new file mode 100755
index 000000000..86e418abb
--- /dev/null
+++ b/sys/src/cmd/wikifs/testwrite.c
@@ -0,0 +1,52 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+char *wikidir = ".";
+
+void
+usage(void)
+{
+ fprint(2, "usage: testwrite [-d dir] wikifile n\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ ulong t;
+ int i;
+ Biobuf *b;
+ String *h;
+ Whist *doc;
+ char tmp[20];
+
+ t = 0;
+ ARGBEGIN{
+ case 't':
+ t = strtoul(EARGF(usage()), 0, 0);
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if(argc != 2)
+ usage();
+
+ if((b = Bopen(argv[0], OREAD)) == nil)
+ sysfatal("Bopen: %r");
+
+ if((doc = Brdwhist(b)) == nil)
+ sysfatal("Brdwtxt: %r");
+
+ sprint(tmp, "D%lud\n", time(0));
+ if((h = pagetext(s_copy(tmp), (doc->doc+doc->ndoc-1)->wtxt, 1))==nil)
+ sysfatal("wiki2text: %r");
+
+ if(writepage(atoi(argv[1]), t, h, doc->title) <0)
+ sysfatal("writepage: %r");
+ exits(0);
+}
diff --git a/sys/src/cmd/wikifs/tohtml.c b/sys/src/cmd/wikifs/tohtml.c
new file mode 100755
index 000000000..716848951
--- /dev/null
+++ b/sys/src/cmd/wikifs/tohtml.c
@@ -0,0 +1,825 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+/*
+ * Get HTML and text templates from underlying file system.
+ * Caches them, which means changes don't take effect for
+ * up to Tcache seconds after they are made.
+ *
+ * If the files are deleted, we keep returning the last
+ * known copy.
+ */
+enum {
+ WAIT = 60
+};
+
+static char *name[2*Ntemplate] = {
+ [Tpage] "page.html",
+ [Tedit] "edit.html",
+ [Tdiff] "diff.html",
+ [Thistory] "history.html",
+ [Tnew] "new.html",
+ [Toldpage] "oldpage.html",
+ [Twerror] "werror.html",
+ [Ntemplate+Tpage] "page.txt",
+ [Ntemplate+Tdiff] "diff.txt",
+ [Ntemplate+Thistory] "history.txt",
+ [Ntemplate+Toldpage] "oldpage.txt",
+ [Ntemplate+Twerror] "werror.txt",
+};
+
+static struct {
+ RWLock;
+ String *s;
+ ulong t;
+ Qid qid;
+} cache[2*Ntemplate];
+
+static void
+cacheinit(void)
+{
+ int i;
+ static int x;
+ static Lock l;
+
+ if(x)
+ return;
+ lock(&l);
+ if(x){
+ unlock(&l);
+ return;
+ }
+
+ for(i=0; i<2*Ntemplate; i++)
+ if(name[i])
+ cache[i].s = s_copy("");
+ x = 1;
+ unlock(&l);
+}
+
+static String*
+gettemplate(int type)
+{
+ int n;
+ Biobuf *b;
+ Dir *d;
+ String *s, *ns;
+
+ if(name[type]==nil)
+ return nil;
+
+ cacheinit();
+
+ rlock(&cache[type]);
+ if(0 && cache[type].t+Tcache >= time(0)){
+ s = s_incref(cache[type].s);
+ runlock(&cache[type]);
+ return s;
+ }
+ runlock(&cache[type]);
+
+// d = nil;
+ wlock(&cache[type]);
+ if(0 && cache[type].t+Tcache >= time(0) || (d = wdirstat(name[type])) == nil)
+ goto Return;
+
+ if(0 && d->qid.vers == cache[type].qid.vers && d->qid.path == cache[type].qid.path){
+ cache[type].t = time(0);
+ goto Return;
+ }
+
+ if((b = wBopen(name[type], OREAD)) == nil)
+ goto Return;
+
+ ns = s_reset(nil);
+ do
+ n = s_read(b, ns, Bsize);
+ while(n > 0);
+ Bterm(b);
+ if(n < 0) {
+ s_free(ns);
+ goto Return;
+ }
+
+ s_free(cache[type].s);
+ cache[type].s = ns;
+ cache[type].qid = d->qid;
+ cache[type].t = time(0);
+
+Return:
+ free(d);
+ s = s_incref(cache[type].s);
+ wunlock(&cache[type]);
+ return s;
+}
+
+
+/*
+ * Write wiki document in HTML.
+ */
+static String*
+s_escappend(String *s, char *p, int pre)
+{
+ char *q;
+
+ while(q = strpbrk(p, pre ? "<>&" : " <>&")){
+ s = s_nappend(s, p, q-p);
+ switch(*q){
+ case '<':
+ s = s_append(s, "&lt;");
+ break;
+ case '>':
+ s = s_append(s, "&gt;");
+ break;
+ case '&':
+ s = s_append(s, "&amp;");
+ break;
+ case ' ':
+ s = s_append(s, "\n");
+ }
+ p = q+1;
+ }
+ s = s_append(s, p);
+ return s;
+}
+
+static char*
+mkurl(char *s, int ty)
+{
+ char *p, *q;
+
+ if(strncmp(s, "http:", 5)==0
+ || strncmp(s, "https:", 6)==0
+ || strncmp(s, "#", 1)==0
+ || strncmp(s, "ftp:", 4)==0
+ || strncmp(s, "mailto:", 7)==0
+ || strncmp(s, "telnet:", 7)==0
+ || strncmp(s, "file:", 5)==0)
+ return estrdup(s);
+
+ if(strchr(s, ' ')==nil && strchr(s, '@')!=nil){
+ p = emalloc(strlen(s)+8);
+ strcpy(p, "mailto:");
+ strcat(p, s);
+ return p;
+ }
+
+ if(ty == Toldpage)
+ p = smprint("../../%s", s);
+ else
+ p = smprint("../%s", s);
+
+ for(q=p; *q; q++)
+ if(*q==' ')
+ *q = '_';
+ return p;
+}
+
+int okayinlist[Nwtxt] =
+{
+ [Wbullet] 1,
+ [Wlink] 1,
+ [Wman] 1,
+ [Wplain] 1,
+};
+
+int okayinpre[Nwtxt] =
+{
+ [Wlink] 1,
+ [Wman] 1,
+ [Wpre] 1,
+};
+
+int okayinpara[Nwtxt] =
+{
+ [Wpara] 1,
+ [Wlink] 1,
+ [Wman] 1,
+ [Wplain] 1,
+};
+
+char*
+nospaces(char *s)
+{
+ char *q;
+ s = strdup(s);
+ if(s == nil)
+ return nil;
+ for(q=s; *q; q++)
+ if(*q == ' ')
+ *q = '_';
+ return s;
+}
+
+String*
+pagehtml(String *s, Wpage *wtxt, int ty)
+{
+ char *p, tmp[40];
+ int inlist, inpara, inpre, t, tnext;
+ Wpage *w;
+
+ inlist = 0;
+ inpre = 0;
+ inpara = 0;
+
+ for(w=wtxt; w; w=w->next){
+ t = w->type;
+ tnext = Whr;
+ if(w->next)
+ tnext = w->next->type;
+
+ if(inlist && !okayinlist[t]){
+ inlist = 0;
+ s = s_append(s, "\n</li>\n</ul>\n");
+ }
+ if(inpre && !okayinpre[t]){
+ inpre = 0;
+ s = s_append(s, "</pre>\n");
+ }
+
+ switch(t){
+ case Wheading:
+ p = nospaces(w->text);
+ s = s_appendlist(s,
+ "\n<a name=\"", p, "\" /><h3>",
+ w->text, "</h3>\n", nil);
+ free(p);
+ break;
+
+ case Wpara:
+ if(inpara){
+ s = s_append(s, "\n</p>\n");
+ inpara = 0;
+ }
+ if(okayinpara[tnext]){
+ s = s_append(s, "\n<p class='para'>\n");
+ inpara = 1;
+ }
+ break;
+
+ case Wbullet:
+ if(!inlist){
+ inlist = 1;
+ s = s_append(s, "\n<ul>\n");
+ }else
+ s = s_append(s, "\n</li>\n");
+ s = s_append(s, "\n<li>\n");
+ break;
+
+ case Wlink:
+ if(w->url == nil)
+ p = mkurl(w->text, ty);
+ else
+ p = w->url;
+ s = s_appendlist(s, "<a href=\"", p, "\">", nil);
+ s = s_escappend(s, w->text, 0);
+ s = s_append(s, "</a>");
+ if(w->url == nil)
+ free(p);
+ break;
+
+ case Wman:
+ sprint(tmp, "%d", w->section);
+ s = s_appendlist(s,
+ "<a href=\"http://plan9.bell-labs.com/magic/man2html/",
+ tmp, "/", w->text, "\"><i>", w->text, "</i>(",
+ tmp, ")</a>", nil);
+ break;
+
+ case Wpre:
+ if(!inpre){
+ inpre = 1;
+ s = s_append(s, "\n<pre>\n");
+ }
+ s = s_escappend(s, w->text, 1);
+ s = s_append(s, "\n");
+ break;
+
+ case Whr:
+ s = s_append(s, "<hr />");
+ break;
+
+ case Wplain:
+ s = s_escappend(s, w->text, 0);
+ break;
+ }
+ }
+ if(inlist)
+ s = s_append(s, "\n</li>\n</ul>\n");
+ if(inpre)
+ s = s_append(s, "</pre>\n");
+ if(inpara)
+ s = s_append(s, "\n</p>\n");
+ return s;
+}
+
+static String*
+copythru(String *s, char **newp, int *nlinep, int l)
+{
+ char *oq, *q, *r;
+ int ol;
+
+ q = *newp;
+ oq = q;
+ ol = *nlinep;
+ while(ol < l){
+ if(r = strchr(q, '\n'))
+ q = r+1;
+ else{
+ q += strlen(q);
+ break;
+ }
+ ol++;
+ }
+ if(*nlinep < l)
+ *nlinep = l;
+ *newp = q;
+ return s_nappend(s, oq, q-oq);
+}
+
+static int
+dodiff(char *f1, char *f2)
+{
+ int p[2];
+
+ if(pipe(p) < 0){
+ return -1;
+ }
+
+ switch(fork()){
+ case -1:
+ return -1;
+
+ case 0:
+ close(p[0]);
+ dup(p[1], 1);
+ execl("/bin/diff", "diff", f1, f2, nil);
+ _exits(nil);
+ }
+ close(p[1]);
+ return p[0];
+}
+
+
+/* print document i grayed out, with only diffs relative to j in black */
+static String*
+s_diff(String *s, Whist *h, int i, int j)
+{
+ char *p, *q, *pnew;
+ int fdiff, fd1, fd2, n1, n2;
+ Biobuf b;
+ char fn1[40], fn2[40];
+ String *new, *old;
+ int nline;
+
+ if(j < 0)
+ return pagehtml(s, h->doc[i].wtxt, Tpage);
+
+ strcpy(fn1, "/tmp/wiki.XXXXXX");
+ strcpy(fn2, "/tmp/wiki.XXXXXX");
+ if((fd1 = opentemp(fn1)) < 0 || (fd2 = opentemp(fn2)) < 0){
+ close(fd1);
+ s = s_append(s, "\nopentemp failed; sorry\n");
+ return s;
+ }
+
+ new = pagehtml(s_reset(nil), h->doc[i].wtxt, Tpage);
+ old = pagehtml(s_reset(nil), h->doc[j].wtxt, Tpage);
+ write(fd1, s_to_c(new), s_len(new));
+ write(fd2, s_to_c(old), s_len(old));
+
+ fdiff = dodiff(fn2, fn1);
+ if(fdiff < 0)
+ s = s_append(s, "\ndiff failed; sorry\n");
+ else{
+ nline = 0;
+ pnew = s_to_c(new);
+ Binit(&b, fdiff, OREAD);
+ while(p = Brdline(&b, '\n')){
+ if(p[0]=='<' || p[0]=='>' || p[0]=='-')
+ continue;
+ p[Blinelen(&b)-1] = '\0';
+ if((p = strpbrk(p, "acd")) == nil)
+ continue;
+ n1 = atoi(p+1);
+ if(q = strchr(p, ','))
+ n2 = atoi(q+1);
+ else
+ n2 = n1;
+ switch(*p){
+ case 'a':
+ case 'c':
+ s = s_append(s, "<span class='old_text'>");
+ s = copythru(s, &pnew, &nline, n1-1);
+ s = s_append(s, "</span><span class='new_text'>");
+ s = copythru(s, &pnew, &nline, n2);
+ s = s_append(s, "</span>");
+ break;
+ }
+ }
+ close(fdiff);
+ s = s_append(s, "<span class='old_text'>");
+ s = s_append(s, pnew);
+ s = s_append(s, "</span>");
+
+ }
+ s_free(new);
+ s_free(old);
+ close(fd1);
+ close(fd2);
+ return s;
+}
+
+static String*
+diffhtml(String *s, Whist *h)
+{
+ int i;
+ char tmp[50];
+ char *atime;
+
+ for(i=h->ndoc-1; i>=0; i--){
+ s = s_append(s, "<hr /><div class='diff_head'>\n");
+ if(i==h->current)
+ sprint(tmp, "index.html");
+ else
+ sprint(tmp, "%lud", h->doc[i].time);
+ atime = ctime(h->doc[i].time);
+ atime[strlen(atime)-1] = '\0';
+ s = s_appendlist(s,
+ "<a href=\"", tmp, "\">",
+ atime, "</a>", nil);
+ if(h->doc[i].author)
+ s = s_appendlist(s, ", ", h->doc[i].author, nil);
+ if(h->doc[i].conflict)
+ s = s_append(s, ", conflicting write");
+ s = s_append(s, "\n");
+ if(h->doc[i].comment)
+ s = s_appendlist(s, "<br /><i>", h->doc[i].comment, "</i>\n", nil);
+ s = s_append(s, "</div><hr />");
+ s = s_diff(s, h, i, i-1);
+ }
+ s = s_append(s, "<hr>");
+ return s;
+}
+
+static String*
+historyhtml(String *s, Whist *h)
+{
+ int i;
+ char tmp[40];
+ char *atime;
+
+ s = s_append(s, "<ul>\n");
+ for(i=h->ndoc-1; i>=0; i--){
+ if(i==h->current)
+ sprint(tmp, "index.html");
+ else
+ sprint(tmp, "%lud", h->doc[i].time);
+ atime = ctime(h->doc[i].time);
+ atime[strlen(atime)-1] = '\0';
+ s = s_appendlist(s,
+ "<li><a href=\"", tmp, "\">",
+ atime, "</a>", nil);
+ if(h->doc[i].author)
+ s = s_appendlist(s, ", ", h->doc[i].author, nil);
+ if(h->doc[i].conflict)
+ s = s_append(s, ", conflicting write");
+ s = s_append(s, "\n");
+ if(h->doc[i].comment)
+ s = s_appendlist(s, "<br><i>", h->doc[i].comment, "</i>\n", nil);
+ }
+ s = s_append(s, "</ul>");
+ return s;
+}
+
+String*
+tohtml(Whist *h, Wdoc *d, int ty)
+{
+ char *atime;
+ char *p, *q, ver[40];
+ int nsub;
+ Sub sub[3];
+ String *s, *t;
+
+ t = gettemplate(ty);
+ if(p = strstr(s_to_c(t), "PAGE"))
+ q = p+4;
+ else{
+ p = s_to_c(t)+s_len(t);
+ q = nil;
+ }
+
+ nsub = 0;
+ if(h){
+ sub[nsub] = (Sub){ "TITLE", h->title };
+ nsub++;
+ }
+ if(d){
+ sprint(ver, "%lud", d->time);
+ sub[nsub] = (Sub){ "VERSION", ver };
+ nsub++;
+ atime = ctime(d->time);
+ atime[strlen(atime)-1] = '\0';
+ sub[nsub] = (Sub){ "DATE", atime };
+ nsub++;
+ }
+
+ s = s_reset(nil);
+ s = s_appendsub(s, s_to_c(t), p-s_to_c(t), sub, nsub);
+ switch(ty){
+ case Tpage:
+ case Toldpage:
+ s = pagehtml(s, d->wtxt, ty);
+ break;
+ case Tedit:
+ s = pagetext(s, d->wtxt, 0);
+ break;
+ case Tdiff:
+ s = diffhtml(s, h);
+ break;
+ case Thistory:
+ s = historyhtml(s, h);
+ break;
+ case Tnew:
+ case Twerror:
+ break;
+ }
+ if(q)
+ s = s_appendsub(s, q, strlen(q), sub, nsub);
+ s_free(t);
+ return s;
+}
+
+enum {
+ LINELEN = 70,
+};
+
+static String*
+s_appendbrk(String *s, char *p, char *prefix, int dosharp)
+{
+ char *e, *w, *x;
+ int first, l;
+ Rune r;
+
+ first = 1;
+ while(*p){
+ s = s_append(s, p);
+ e = strrchr(s_to_c(s), '\n');
+ if(e == nil)
+ e = s_to_c(s);
+ else
+ e++;
+ if(utflen(e) <= LINELEN)
+ break;
+ x = e; l=LINELEN;
+ while(l--)
+ x+=chartorune(&r, x);
+ x = strchr(x, ' ');
+ if(x){
+ *x = '\0';
+ w = strrchr(e, ' ');
+ *x = ' ';
+ }else
+ w = strrchr(e, ' ');
+
+ if(w-s_to_c(s) < strlen(prefix))
+ break;
+
+ x = estrdup(w+1);
+ *w = '\0';
+ s->ptr = w;
+ s_append(s, "\n");
+ if(dosharp)
+ s_append(s, "#");
+ s_append(s, prefix);
+ if(!first)
+ free(p);
+ first = 0;
+ p = x;
+ }
+ if(!first)
+ free(p);
+ return s;
+}
+
+static void
+s_endline(String *s, int dosharp)
+{
+ if(dosharp){
+ if(s->ptr == s->base+1 && s->ptr[-1] == '#')
+ return;
+
+ if(s->ptr > s->base+1 && s->ptr[-1] == '#' && s->ptr[-2] == '\n')
+ return;
+ s_append(s, "\n#");
+ }else{
+ if(s->ptr > s->base+1 && s->ptr[-1] == '\n')
+ return;
+ s_append(s, "\n");
+ }
+}
+
+String*
+pagetext(String *s, Wpage *page, int dosharp)
+{
+ int inlist, inpara;
+ char *prefix, *sharp, tmp[40];
+ String *t;
+ Wpage *w;
+
+ inlist = 0;
+ inpara = 0;
+ prefix = "";
+ sharp = dosharp ? "#" : "";
+ s = s_append(s, sharp);
+ for(w=page; w; w=w->next){
+ switch(w->type){
+ case Wheading:
+ if(inlist){
+ prefix = "";
+ inlist = 0;
+ }
+ s_endline(s, dosharp);
+ if(!inpara){
+ inpara = 1;
+ s = s_appendlist(s, "\n", sharp, nil);
+ }
+ s = s_appendlist(s, w->text, "\n", sharp, "\n", sharp, nil);
+ break;
+
+ case Wpara:
+ s_endline(s, dosharp);
+ if(inlist){
+ prefix = "";
+ inlist = 0;
+ }
+ if(!inpara){
+ inpara = 1;
+ s = s_appendlist(s, "\n", sharp, nil);
+ }
+ break;
+
+ case Wbullet:
+ s_endline(s, dosharp);
+ if(!inlist)
+ inlist = 1;
+ if(inpara)
+ inpara = 0;
+ s = s_append(s, " *\t");
+ prefix = "\t";
+ break;
+
+ case Wlink:
+ if(inpara)
+ inpara = 0;
+ t = s_append(s_copy("["), w->text);
+ if(w->url == nil)
+ t = s_append(t, "]");
+ else{
+ t = s_append(t, " | ");
+ t = s_append(t, w->url);
+ t = s_append(t, "]");
+ }
+ s = s_appendbrk(s, s_to_c(t), prefix, dosharp);
+ s_free(t);
+ break;
+
+ case Wman:
+ if(inpara)
+ inpara = 0;
+ s = s_appendbrk(s, w->text, prefix, dosharp);
+ sprint(tmp, "(%d)", w->section);
+ s = s_appendbrk(s, tmp, prefix, dosharp);
+ break;
+
+ case Wpre:
+ if(inlist){
+ prefix = "";
+ inlist = 0;
+ }
+ if(inpara)
+ inpara = 0;
+ s_endline(s, dosharp);
+ s = s_appendlist(s, "! ", w->text, "\n", sharp, nil);
+ break;
+ case Whr:
+ s_endline(s, dosharp);
+ s = s_appendlist(s, "------------------------------------------------------ \n", sharp, nil);
+ break;
+
+ case Wplain:
+ if(inpara)
+ inpara = 0;
+ s = s_appendbrk(s, w->text, prefix, dosharp);
+ break;
+ }
+ }
+ s_endline(s, dosharp);
+ s->ptr--;
+ *s->ptr = '\0';
+ return s;
+}
+
+static String*
+historytext(String *s, Whist *h)
+{
+ int i;
+ char tmp[40];
+ char *atime;
+
+ for(i=h->ndoc-1; i>=0; i--){
+ if(i==h->current)
+ sprint(tmp, "[current]");
+ else
+ sprint(tmp, "[%lud/]", h->doc[i].time);
+ atime = ctime(h->doc[i].time);
+ atime[strlen(atime)-1] = '\0';
+ s = s_appendlist(s, " * ", tmp, " ", atime, nil);
+ if(h->doc[i].author)
+ s = s_appendlist(s, ", ", h->doc[i].author, nil);
+ if(h->doc[i].conflict)
+ s = s_append(s, ", conflicting write");
+ s = s_append(s, "\n");
+ if(h->doc[i].comment)
+ s = s_appendlist(s, "<i>", h->doc[i].comment, "</i>\n", nil);
+ }
+ return s;
+}
+
+String*
+totext(Whist *h, Wdoc *d, int ty)
+{
+ char *atime;
+ char *p, *q, ver[40];
+ int nsub;
+ Sub sub[3];
+ String *s, *t;
+
+ t = gettemplate(Ntemplate+ty);
+ if(p = strstr(s_to_c(t), "PAGE"))
+ q = p+4;
+ else{
+ p = s_to_c(t)+s_len(t);
+ q = nil;
+ }
+
+ nsub = 0;
+ if(h){
+ sub[nsub] = (Sub){ "TITLE", h->title };
+ nsub++;
+ }
+ if(d){
+ sprint(ver, "%lud", d->time);
+ sub[nsub] = (Sub){ "VERSION", ver };
+ nsub++;
+ atime = ctime(d->time);
+ atime[strlen(atime)-1] = '\0';
+ sub[nsub] = (Sub){ "DATE", atime };
+ nsub++;
+ }
+
+ s = s_reset(nil);
+ s = s_appendsub(s, s_to_c(t), p-s_to_c(t), sub, nsub);
+ switch(ty){
+ case Tpage:
+ case Toldpage:
+ s = pagetext(s, d->wtxt, 0);
+ break;
+ case Thistory:
+ s = historytext(s, h);
+ break;
+ case Tnew:
+ case Twerror:
+ break;
+ }
+ if(q)
+ s = s_appendsub(s, q, strlen(q), sub, nsub);
+ s_free(t);
+ return s;
+}
+
+String*
+doctext(String *s, Wdoc *d)
+{
+ char tmp[40];
+
+ sprint(tmp, "D%lud", d->time);
+ s = s_append(s, tmp);
+ if(d->comment){
+ s = s_append(s, "\nC");
+ s = s_append(s, d->comment);
+ }
+ if(d->author){
+ s = s_append(s, "\nA");
+ s = s_append(s, d->author);
+ }
+ if(d->conflict)
+ s = s_append(s, "\nX");
+ s = s_append(s, "\n");
+ s = pagetext(s, d->wtxt, 1);
+ return s;
+}
diff --git a/sys/src/cmd/wikifs/util.c b/sys/src/cmd/wikifs/util.c
new file mode 100755
index 000000000..d108cfb83
--- /dev/null
+++ b/sys/src/cmd/wikifs/util.c
@@ -0,0 +1,133 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <ctype.h>
+#include <thread.h>
+#include "wiki.h"
+
+void*
+erealloc(void *v, ulong n)
+{
+ v = realloc(v, n);
+ if(v == nil)
+ sysfatal("out of memory reallocating %lud", n);
+ setmalloctag(v, getcallerpc(&v));
+ return v;
+}
+
+void*
+emalloc(ulong n)
+{
+ void *v;
+
+ v = malloc(n);
+ if(v == nil)
+ sysfatal("out of memory allocating %lud", n);
+ memset(v, 0, n);
+ setmalloctag(v, getcallerpc(&n));
+ return v;
+}
+
+char*
+estrdup(char *s)
+{
+ int l;
+ char *t;
+
+ if (s == nil)
+ return nil;
+ l = strlen(s)+1;
+ t = emalloc(l);
+ memmove(t, s, l);
+ setmalloctag(t, getcallerpc(&s));
+ return t;
+}
+
+char*
+estrdupn(char *s, int n)
+{
+ int l;
+ char *t;
+
+ l = strlen(s);
+ if(l > n)
+ l = n;
+ t = emalloc(l+1);
+ memmove(t, s, l);
+ t[l] = '\0';
+ setmalloctag(t, getcallerpc(&s));
+ return t;
+}
+
+char*
+strlower(char *s)
+{
+ char *p;
+
+ for(p=s; *p; p++)
+ if('A' <= *p && *p <= 'Z')
+ *p += 'a'-'A';
+ return s;
+}
+
+String*
+s_appendsub(String *s, char *p, int n, Sub *sub, int nsub)
+{
+ int i, m;
+ char *q, *r, *ep;
+
+ ep = p+n;
+ while(p<ep){
+ q = ep;
+ m = -1;
+ for(i=0; i<nsub; i++){
+ if(sub[i].sub && (r = strstr(p, sub[i].match)) && r < q){
+ q = r;
+ m = i;
+ }
+ }
+ s = s_nappend(s, p, q-p);
+ p = q;
+ if(m >= 0){
+ s = s_append(s, sub[m].sub);
+ p += strlen(sub[m].match);
+ }
+ }
+ return s;
+}
+
+String*
+s_appendlist(String *s, ...)
+{
+ char *x;
+ va_list arg;
+
+ va_start(arg, s);
+ while(x = va_arg(arg, char*))
+ s = s_append(s, x);
+ va_end(arg);
+ return s;
+}
+
+int
+opentemp(char *template)
+{
+ int fd, i;
+ char *p;
+
+ p = estrdup(template);
+ fd = -1;
+ for(i=0; i<10; i++){
+ mktemp(p);
+ if(access(p, 0) < 0 && (fd=create(p, ORDWR|ORCLOSE, 0444)) >= 0)
+ break;
+ strcpy(p, template);
+ }
+ if(fd >= 0)
+ strcpy(template, p);
+ free(p);
+
+ return fd;
+}
+
diff --git a/sys/src/cmd/wikifs/wdir.c b/sys/src/cmd/wikifs/wdir.c
new file mode 100755
index 000000000..a2b4f6b26
--- /dev/null
+++ b/sys/src/cmd/wikifs/wdir.c
@@ -0,0 +1,76 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+/* open, create relative to wiki dir */
+char *wikidir;
+
+static char*
+wname(char *s)
+{
+ char *t;
+
+ t = emalloc(strlen(wikidir)+1+strlen(s)+1);
+ strcpy(t, wikidir);
+ strcat(t, "/");
+ strcat(t, s);
+ return t;
+}
+
+int
+wopen(char *fn, int mode)
+{
+ int rv;
+
+ fn = wname(fn);
+ rv = open(fn, mode);
+ free(fn);
+ return rv;
+}
+
+int
+wcreate(char *fn, int mode, long perm)
+{
+ int rv;
+
+ fn = wname(fn);
+ rv = create(fn, mode, perm);
+ free(fn);
+ return rv;
+}
+
+Biobuf*
+wBopen(char *fn, int mode)
+{
+ Biobuf *rv;
+
+ fn = wname(fn);
+ rv = Bopen(fn, mode);
+ free(fn);
+ return rv;
+}
+
+int
+waccess(char *fn, int mode)
+{
+ int rv;
+
+ fn = wname(fn);
+ rv = access(fn, mode);
+ free(fn);
+ return rv;
+}
+
+Dir*
+wdirstat(char *fn)
+{
+ Dir *d;
+
+ fn = wname(fn);
+ d = dirstat(fn);
+ free(fn);
+ return d;
+}
diff --git a/sys/src/cmd/wikifs/wiki.h b/sys/src/cmd/wikifs/wiki.h
new file mode 100755
index 000000000..88cb53388
--- /dev/null
+++ b/sys/src/cmd/wikifs/wiki.h
@@ -0,0 +1,121 @@
+typedef struct Map Map;
+typedef struct Mapel Mapel;
+typedef struct Sub Sub;
+typedef struct Wdoc Wdoc;
+typedef struct Whist Whist;
+typedef struct Wpage Wpage;
+
+enum {
+ Tcache = 5, /* seconds */
+ Maxmap = 10*1024*1024,
+ Maxfile = 100*1024,
+};
+enum {
+ Wpara,
+ Wheading,
+ Wbullet,
+ Wlink,
+ Wman,
+ Wplain,
+ Wpre,
+ Whr,
+ Nwtxt,
+};
+
+struct Wpage {
+ int type;
+ char *text;
+ int section; /* Wman */
+ char *url; /* Wlink */
+ Wpage *next;
+};
+
+struct Whist {
+ Ref;
+ int n;
+ char *title;
+ Wdoc *doc;
+ int ndoc;
+ int current;
+};
+
+struct Wdoc {
+ char *author;
+ char *comment;
+ int conflict;
+ ulong time;
+ Wpage *wtxt;
+};
+
+enum {
+ Tpage,
+ Tedit,
+ Tdiff,
+ Thistory,
+ Tnew,
+ Toldpage,
+ Twerror,
+ Ntemplate,
+};
+
+struct Sub {
+ char *match;
+ char *sub;
+};
+
+struct Mapel {
+ char *s;
+ int n;
+};
+
+struct Map {
+ Ref;
+ Mapel *el;
+ int nel;
+ ulong t;
+ char *buf;
+ Qid qid;
+};
+
+void *erealloc(void*, ulong);
+void *emalloc(ulong);
+char *estrdup(char*);
+char *estrdupn(char*, int);
+char *strcondense(char*, int);
+char *strlower(char*);
+
+String *s_appendsub(String*, char*, int, Sub*, int);
+String *s_appendlist(String*, ...);
+Whist *Brdwhist(Biobuf*);
+Wpage *Brdpage(char*(*)(void*,int), void*);
+
+void printpage(Wpage*);
+String *pagehtml(String*, Wpage*, int);
+String *pagetext(String*, Wpage*, int);
+String *tohtml(Whist*, Wdoc*, int);
+String *totext(Whist*, Wdoc*, int);
+String *doctext(String*, Wdoc*);
+
+Whist *getcurrent(int);
+Whist *getcurrentbyname(char*);
+Whist *gethistory(int);
+void closewhist(Whist*);
+int allocnum(char*, int);
+void freepage(Wpage*);
+int nametonum(char*);
+char *numtoname(int);
+int writepage(int, ulong, String*, char*);
+void voidcache(int);
+
+void closemap(Map*);
+void currentmap(int);
+
+extern Map *map;
+extern RWLock maplock;
+extern char *wikidir;
+Biobuf *wBopen(char*, int);
+int wopen(char*, int);
+int wcreate(char*, int, long);
+int waccess(char*, int);
+Dir *wdirstat(char*);
+int opentemp(char*);
diff --git a/sys/src/cmd/wikifs/wiki2html.c b/sys/src/cmd/wikifs/wiki2html.c
new file mode 100755
index 000000000..902a1942f
--- /dev/null
+++ b/sys/src/cmd/wikifs/wiki2html.c
@@ -0,0 +1,67 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+char *wikidir;
+
+void
+usage(void)
+{
+ fprint(2, "usage: wiki2html [-hoDP ] [-d dir] wikifile\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ int t;
+ int parse;
+ String *h;
+ Whist *doc;
+
+ rfork(RFNAMEG);
+
+ t = Tpage;
+ ARGBEGIN{
+ default:
+ usage();
+ case 'd':
+ wikidir = EARGF(usage());
+ break;
+ case 'h':
+ t = Thistory;
+ break;
+ case 'o':
+ t = Toldpage;
+ break;
+ case 'D':
+ t = Tdiff;
+ break;
+ case 'P':
+ parse = 1;
+ }ARGEND
+
+ if(argc != 1)
+ usage();
+
+ if(t == Thistory || t==Tdiff)
+ doc = gethistory(atoi(argv[0]));
+ else
+ doc = getcurrent(atoi(argv[0]));
+
+ if(doc == nil)
+ sysfatal("doc: %r");
+
+ if(parse){
+ printpage(doc->doc->wtxt);
+ exits(0);
+ }
+ if((h = tohtml(doc, doc->doc+doc->ndoc-1, t)) == nil)
+ sysfatal("wiki2html: %r");
+
+ write(1, s_to_c(h), s_len(h));
+ exits(0);
+}
diff --git a/sys/src/cmd/wikifs/wiki2text.c b/sys/src/cmd/wikifs/wiki2text.c
new file mode 100755
index 000000000..33b33fc69
--- /dev/null
+++ b/sys/src/cmd/wikifs/wiki2text.c
@@ -0,0 +1,50 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <thread.h>
+#include "wiki.h"
+
+char *wikidir = ".";
+
+void
+usage(void)
+{
+ fprint(2, "usage: wiki2text [-d dir] wikifile\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ int i;
+ Biobuf *b;
+ String *h;
+ Whist *doc;
+
+ ARGBEGIN{
+ default:
+ usage();
+ case 'd':
+ wikidir = EARGF(usage());
+ break;
+ }ARGEND
+
+ if(argc != 1)
+ usage();
+
+ if((b = Bopen(argv[0], OREAD)) == nil)
+ sysfatal("Bopen: %r");
+
+ if((doc = Brdwhist(b)) == nil)
+ sysfatal("Brdwtxt: %r");
+
+ h = nil;
+ for(i=0; i<doc->ndoc; i++){
+ print("__________________ %d ______________\n", i);
+ if((h = pagetext(s_reset(h), doc->doc[i].wtxt, 1)) == nil)
+ sysfatal("wiki2html: %r");
+ write(1, s_to_c(h), s_len(h));
+ }
+ exits(0);
+}