xref: /plan9/sys/src/cmd/wikifs/tohtml.c (revision 1052a86abe4783012df9e7959032cbd3f59e6d9c)
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <String.h>
5 #include <thread.h>
6 #include "wiki.h"
7 
8 /*
9  * Get HTML and text templates from underlying file system.
10  * Caches them, which means changes don't take effect for
11  * up to Tcache seconds after they are made.
12  *
13  * If the files are deleted, we keep returning the last
14  * known copy.
15  */
16 enum {
17 	WAIT = 60
18 };
19 
20 static char *name[2*Ntemplate] = {
21  [Tpage]		"page.html",
22  [Tedit]		"edit.html",
23  [Tdiff]		"diff.html",
24  [Thistory]		"history.html",
25  [Tnew]		"new.html",
26  [Toldpage]	"oldpage.html",
27  [Twerror]		"werror.html",
28  [Ntemplate+Tpage]	"page.txt",
29  [Ntemplate+Tdiff]	"diff.txt",
30  [Ntemplate+Thistory]	"history.txt",
31  [Ntemplate+Toldpage]	"oldpage.txt",
32  [Ntemplate+Twerror]	"werror.txt",
33 };
34 
35 static struct {
36 	RWLock;
37 	String *s;
38 	ulong t;
39 	Qid qid;
40 } cache[2*Ntemplate];
41 
42 static void
cacheinit(void)43 cacheinit(void)
44 {
45 	int i;
46 	static int x;
47 	static Lock l;
48 
49 	if(x)
50 		return;
51 	lock(&l);
52 	if(x){
53 		unlock(&l);
54 		return;
55 	}
56 
57 	for(i=0; i<2*Ntemplate; i++)
58 		if(name[i])
59 			cache[i].s = s_copy("");
60 	x = 1;
61 	unlock(&l);
62 }
63 
64 static String*
gettemplate(int type)65 gettemplate(int type)
66 {
67 	int n;
68 	Biobuf *b;
69 	Dir *d;
70 	String *s, *ns;
71 
72 	if(name[type]==nil)
73 		return nil;
74 
75 	cacheinit();
76 
77 	rlock(&cache[type]);
78 	if(0 && cache[type].t+Tcache >= time(0)){
79 		s = s_incref(cache[type].s);
80 		runlock(&cache[type]);
81 		return s;
82 	}
83 	runlock(&cache[type]);
84 
85 //	d = nil;
86 	wlock(&cache[type]);
87 	if(0 && cache[type].t+Tcache >= time(0) || (d = wdirstat(name[type])) == nil)
88 		goto Return;
89 
90 	if(0 && d->qid.vers == cache[type].qid.vers && d->qid.path == cache[type].qid.path){
91 		cache[type].t = time(0);
92 		goto Return;
93 	}
94 
95 	if((b = wBopen(name[type], OREAD)) == nil)
96 		goto Return;
97 
98 	ns = s_reset(nil);
99 	do
100 		n = s_read(b, ns, Bsize);
101 	while(n > 0);
102 	Bterm(b);
103 	if(n < 0) {
104 		s_free(ns);
105 		goto Return;
106 	}
107 
108 	s_free(cache[type].s);
109 	cache[type].s = ns;
110 	cache[type].qid = d->qid;
111 	cache[type].t = time(0);
112 
113 Return:
114 	free(d);
115 	s = s_incref(cache[type].s);
116 	wunlock(&cache[type]);
117 	return s;
118 }
119 
120 
121 /*
122  * Write wiki document in HTML.
123  */
124 static String*
s_escappend(String * s,char * p,int pre)125 s_escappend(String *s, char *p, int pre)
126 {
127 	char *q;
128 
129 	while(q = strpbrk(p, pre ? "<>&" : " <>&")){
130 		s = s_nappend(s, p, q-p);
131 		switch(*q){
132 		case '<':
133 			s = s_append(s, "&lt;");
134 			break;
135 		case '>':
136 			s = s_append(s, "&gt;");
137 			break;
138 		case '&':
139 			s = s_append(s, "&amp;");
140 			break;
141 		case ' ':
142 			s = s_append(s, "\n");
143 		}
144 		p = q+1;
145 	}
146 	s = s_append(s, p);
147 	return s;
148 }
149 
150 static char*
mkurl(char * s,int ty)151 mkurl(char *s, int ty)
152 {
153 	char *p, *q;
154 
155 	if(strncmp(s, "http:", 5)==0
156 	|| strncmp(s, "https:", 6)==0
157 	|| strncmp(s, "#", 1)==0
158 	|| strncmp(s, "ftp:", 4)==0
159 	|| strncmp(s, "mailto:", 7)==0
160 	|| strncmp(s, "telnet:", 7)==0
161 	|| strncmp(s, "file:", 5)==0)
162 		return estrdup(s);
163 
164 	if(strchr(s, ' ')==nil && strchr(s, '@')!=nil){
165 		p = emalloc(strlen(s)+8);
166 		strcpy(p, "mailto:");
167 		strcat(p, s);
168 		return p;
169 	}
170 
171 	if(ty == Toldpage)
172 		p = smprint("../../%s", s);
173 	else
174 		p = smprint("../%s", s);
175 
176 	for(q=p; *q; q++)
177 		if(*q==' ')
178 			*q = '_';
179 	return p;
180 }
181 
182 int okayinlist[Nwtxt] =
183 {
184 	[Wbullet]	1,
185 	[Wlink]	1,
186 	[Wman]	1,
187 	[Wplain]	1,
188 };
189 
190 int okayinpre[Nwtxt] =
191 {
192 	[Wlink]	1,
193 	[Wman]	1,
194 	[Wpre]	1,
195 };
196 
197 int okayinpara[Nwtxt] =
198 {
199 	[Wpara]	1,
200 	[Wlink]	1,
201 	[Wman]	1,
202 	[Wplain]	1,
203 };
204 
205 char*
nospaces(char * s)206 nospaces(char *s)
207 {
208 	char *q;
209 	s = strdup(s);
210 	if(s == nil)
211 		return nil;
212 	for(q=s; *q; q++)
213 		if(*q == ' ')
214 			*q = '_';
215 	return s;
216 }
217 
218 String*
pagehtml(String * s,Wpage * wtxt,int ty)219 pagehtml(String *s, Wpage *wtxt, int ty)
220 {
221 	char *p, tmp[40];
222 	int inlist, inpara, inpre, t, tnext;
223 	Wpage *w;
224 
225 	inlist = 0;
226 	inpre = 0;
227 	inpara = 0;
228 
229 	for(w=wtxt; w; w=w->next){
230 		t = w->type;
231 		tnext = Whr;
232 		if(w->next)
233 			tnext = w->next->type;
234 
235 		if(inlist && !okayinlist[t]){
236 			inlist = 0;
237 			s = s_append(s, "\n</li>\n</ul>\n");
238 		}
239 		if(inpre && !okayinpre[t]){
240 			inpre = 0;
241 			s = s_append(s, "</pre>\n");
242 		}
243 
244 		switch(t){
245 		case Wheading:
246 			p = nospaces(w->text);
247 			s = s_appendlist(s,
248 				"\n<a name=\"", p, "\" /><h3>",
249 				w->text, "</h3>\n", nil);
250 			free(p);
251 			break;
252 
253 		case Wpara:
254 			if(inpara){
255 				s = s_append(s, "\n</p>\n");
256 				inpara = 0;
257 			}
258 			if(okayinpara[tnext]){
259 				s = s_append(s, "\n<p class='para'>\n");
260 				inpara = 1;
261 			}
262 			break;
263 
264 		case Wbullet:
265 			if(!inlist){
266 				inlist = 1;
267 				s = s_append(s, "\n<ul>\n");
268 			}else
269 				s = s_append(s, "\n</li>\n");
270 			s = s_append(s, "\n<li>\n");
271 			break;
272 
273 		case Wlink:
274 			if(w->url == nil)
275 				p = mkurl(w->text, ty);
276 			else
277 				p = w->url;
278 			s = s_appendlist(s, "<a href=\"", p, "\">", nil);
279 			s = s_escappend(s, w->text, 0);
280 			s = s_append(s, "</a>");
281 			if(w->url == nil)
282 				free(p);
283 			break;
284 
285 		case Wman:
286 			sprint(tmp, "%d", w->section);
287 			s = s_appendlist(s,
288 				"<a href=\"http://plan9.bell-labs.com/magic/man2html/",
289 				tmp, "/", w->text, "\"><i>", w->text, "</i>(",
290 				tmp, ")</a>", nil);
291 			break;
292 
293 		case Wpre:
294 			if(!inpre){
295 				inpre = 1;
296 				s = s_append(s, "\n<pre>\n");
297 			}
298 			s = s_escappend(s, w->text, 1);
299 			s = s_append(s, "\n");
300 			break;
301 
302 		case Whr:
303 			s = s_append(s, "<hr />");
304 			break;
305 
306 		case Wplain:
307 			s = s_escappend(s, w->text, 0);
308 			break;
309 		}
310 	}
311 	if(inlist)
312 		s = s_append(s, "\n</li>\n</ul>\n");
313 	if(inpre)
314 		s = s_append(s, "</pre>\n");
315 	if(inpara)
316 		s = s_append(s, "\n</p>\n");
317 	return s;
318 }
319 
320 static String*
copythru(String * s,char ** newp,int * nlinep,int l)321 copythru(String *s, char **newp, int *nlinep, int l)
322 {
323 	char *oq, *q, *r;
324 	int ol;
325 
326 	q = *newp;
327 	oq = q;
328 	ol = *nlinep;
329 	while(ol < l){
330 		if(r = strchr(q, '\n'))
331 			q = r+1;
332 		else{
333 			q += strlen(q);
334 			break;
335 		}
336 		ol++;
337 	}
338 	if(*nlinep < l)
339 		*nlinep = l;
340 	*newp = q;
341 	return s_nappend(s, oq, q-oq);
342 }
343 
344 static int
dodiff(char * f1,char * f2)345 dodiff(char *f1, char *f2)
346 {
347 	int p[2];
348 
349 	if(pipe(p) < 0){
350 		return -1;
351 	}
352 
353 	switch(fork()){
354 	case -1:
355 		return -1;
356 
357 	case 0:
358 		close(p[0]);
359 		dup(p[1], 1);
360 		execl("/bin/diff", "diff", f1, f2, nil);
361 		_exits(nil);
362 	}
363 	close(p[1]);
364 	return p[0];
365 }
366 
367 
368 /* print document i grayed out, with only diffs relative to j in black */
369 static String*
s_diff(String * s,Whist * h,int i,int j)370 s_diff(String *s, Whist *h, int i, int j)
371 {
372 	char *p, *q, *pnew;
373 	int fdiff, fd1, fd2, n1, n2;
374 	Biobuf b;
375 	char fn1[40], fn2[40];
376 	String *new, *old;
377 	int nline;
378 
379 	if(j < 0)
380 		return pagehtml(s, h->doc[i].wtxt, Tpage);
381 
382 	strcpy(fn1, "/tmp/wiki.XXXXXX");
383 	strcpy(fn2, "/tmp/wiki.XXXXXX");
384 	if((fd1 = opentemp(fn1)) < 0 || (fd2 = opentemp(fn2)) < 0){
385 		close(fd1);
386 		s = s_append(s, "\nopentemp failed; sorry\n");
387 		return s;
388 	}
389 
390 	new = pagehtml(s_reset(nil), h->doc[i].wtxt, Tpage);
391 	old = pagehtml(s_reset(nil), h->doc[j].wtxt, Tpage);
392 	write(fd1, s_to_c(new), s_len(new));
393 	write(fd2, s_to_c(old), s_len(old));
394 
395 	fdiff = dodiff(fn2, fn1);
396 	if(fdiff < 0)
397 		s = s_append(s, "\ndiff failed; sorry\n");
398 	else{
399 		nline = 0;
400 		pnew = s_to_c(new);
401 		Binit(&b, fdiff, OREAD);
402 		while(p = Brdline(&b, '\n')){
403 			if(p[0]=='<' || p[0]=='>' || p[0]=='-')
404 				continue;
405 			p[Blinelen(&b)-1] = '\0';
406 			if((p = strpbrk(p, "acd")) == nil)
407 				continue;
408 			n1 = atoi(p+1);
409 			if(q = strchr(p, ','))
410 				n2 = atoi(q+1);
411 			else
412 				n2 = n1;
413 			switch(*p){
414 			case 'a':
415 			case 'c':
416 				s = s_append(s, "<span class='old_text'>");
417 				s = copythru(s, &pnew, &nline, n1-1);
418 				s = s_append(s, "</span><span class='new_text'>");
419 				s = copythru(s, &pnew, &nline, n2);
420 				s = s_append(s, "</span>");
421 				break;
422 			}
423 		}
424 		close(fdiff);
425 		s = s_append(s, "<span class='old_text'>");
426 		s = s_append(s, pnew);
427 		s = s_append(s, "</span>");
428 
429 	}
430 	s_free(new);
431 	s_free(old);
432 	close(fd1);
433 	close(fd2);
434 	return s;
435 }
436 
437 static String*
diffhtml(String * s,Whist * h)438 diffhtml(String *s, Whist *h)
439 {
440 	int i;
441 	char tmp[50];
442 	char *atime;
443 
444 	for(i=h->ndoc-1; i>=0; i--){
445 		s = s_append(s, "<hr /><div class='diff_head'>\n");
446 		if(i==h->current)
447 			sprint(tmp, "index.html");
448 		else
449 			sprint(tmp, "%lud", h->doc[i].time);
450 		atime = ctime(h->doc[i].time);
451 		atime[strlen(atime)-1] = '\0';
452 		s = s_appendlist(s,
453 			"<a href=\"", tmp, "\">",
454 			atime, "</a>", nil);
455 		if(h->doc[i].author)
456 			s = s_appendlist(s, ", ", h->doc[i].author, nil);
457 		if(h->doc[i].conflict)
458 			s = s_append(s, ", conflicting write");
459 		s = s_append(s, "\n");
460 		if(h->doc[i].comment)
461 			s = s_appendlist(s, "<br /><i>", h->doc[i].comment, "</i>\n", nil);
462 		s = s_append(s, "</div><hr />");
463 		s = s_diff(s, h, i, i-1);
464 	}
465 	s = s_append(s, "<hr>");
466 	return s;
467 }
468 
469 static String*
historyhtml(String * s,Whist * h)470 historyhtml(String *s, Whist *h)
471 {
472 	int i;
473 	char tmp[40];
474 	char *atime;
475 
476 	s = s_append(s, "<ul>\n");
477 	for(i=h->ndoc-1; i>=0; i--){
478 		if(i==h->current)
479 			sprint(tmp, "index.html");
480 		else
481 			sprint(tmp, "%lud", h->doc[i].time);
482 		atime = ctime(h->doc[i].time);
483 		atime[strlen(atime)-1] = '\0';
484 		s = s_appendlist(s,
485 			"<li><a href=\"", tmp, "\">",
486 			atime, "</a>", nil);
487 		if(h->doc[i].author)
488 			s = s_appendlist(s, ", ", h->doc[i].author, nil);
489 		if(h->doc[i].conflict)
490 			s = s_append(s, ", conflicting write");
491 		s = s_append(s, "\n");
492 		if(h->doc[i].comment)
493 			s = s_appendlist(s, "<br><i>", h->doc[i].comment, "</i>\n", nil);
494 	}
495 	s = s_append(s, "</ul>");
496 	return s;
497 }
498 
499 String*
tohtml(Whist * h,Wdoc * d,int ty)500 tohtml(Whist *h, Wdoc *d, int ty)
501 {
502 	char *atime;
503 	char *p, *q, ver[40];
504 	int nsub;
505 	Sub sub[3];
506 	String *s, *t;
507 
508 	t = gettemplate(ty);
509 	if(p = strstr(s_to_c(t), "PAGE"))
510 		q = p+4;
511 	else{
512 		p = s_to_c(t)+s_len(t);
513 		q = nil;
514 	}
515 
516 	nsub = 0;
517 	if(h){
518 		sub[nsub] = (Sub){ "TITLE", h->title };
519 		nsub++;
520 	}
521 	if(d){
522 		sprint(ver, "%lud", d->time);
523 		sub[nsub] = (Sub){ "VERSION", ver };
524 		nsub++;
525 		atime = ctime(d->time);
526 		atime[strlen(atime)-1] = '\0';
527 		sub[nsub] = (Sub){ "DATE", atime };
528 		nsub++;
529 	}
530 
531 	s = s_reset(nil);
532 	s = s_appendsub(s, s_to_c(t), p-s_to_c(t), sub, nsub);
533 	switch(ty){
534 	case Tpage:
535 	case Toldpage:
536 		s = pagehtml(s, d->wtxt, ty);
537 		break;
538 	case Tedit:
539 		s = pagetext(s, d->wtxt, 0);
540 		break;
541 	case Tdiff:
542 		s = diffhtml(s, h);
543 		break;
544 	case Thistory:
545 		s = historyhtml(s, h);
546 		break;
547 	case Tnew:
548 	case Twerror:
549 		break;
550 	}
551 	if(q)
552 		s = s_appendsub(s, q, strlen(q), sub, nsub);
553 	s_free(t);
554 	return s;
555 }
556 
557 enum {
558 	LINELEN = 70,
559 };
560 
561 static String*
s_appendbrk(String * s,char * p,char * prefix,int dosharp)562 s_appendbrk(String *s, char *p, char *prefix, int dosharp)
563 {
564 	char *e, *w, *x;
565 	int first, l;
566 	Rune r;
567 
568 	first = 1;
569 	while(*p){
570 		s = s_append(s, p);
571 		e = strrchr(s_to_c(s), '\n');
572 		if(e == nil)
573 			e = s_to_c(s);
574 		else
575 			e++;
576 		if(utflen(e) <= LINELEN)
577 			break;
578 		x = e; l=LINELEN;
579 		while(l--)
580 			x+=chartorune(&r, x);
581 		x = strchr(x, ' ');
582 		if(x){
583 			*x = '\0';
584 			w = strrchr(e, ' ');
585 			*x = ' ';
586 		}else
587 			w = strrchr(e, ' ');
588 
589 		if(w-s_to_c(s) < strlen(prefix))
590 			break;
591 
592 		x = estrdup(w+1);
593 		*w = '\0';
594 		s->ptr = w;
595 		s_append(s, "\n");
596 		if(dosharp)
597 			s_append(s, "#");
598 		s_append(s, prefix);
599 		if(!first)
600 			free(p);
601 		first = 0;
602 		p = x;
603 	}
604 	if(!first)
605 		free(p);
606 	return s;
607 }
608 
609 static void
s_endline(String * s,int dosharp)610 s_endline(String *s, int dosharp)
611 {
612 	if(dosharp){
613 		if(s->ptr == s->base+1 && s->ptr[-1] == '#')
614 			return;
615 
616 		if(s->ptr > s->base+1 && s->ptr[-1] == '#' && s->ptr[-2] == '\n')
617 			return;
618 		s_append(s, "\n#");
619 	}else{
620 		if(s->ptr > s->base+1 && s->ptr[-1] == '\n')
621 			return;
622 		s_append(s, "\n");
623 	}
624 }
625 
626 String*
pagetext(String * s,Wpage * page,int dosharp)627 pagetext(String *s, Wpage *page, int dosharp)
628 {
629 	int inlist, inpara;
630 	char *prefix, *sharp, tmp[40];
631 	String *t;
632 	Wpage *w;
633 
634 	inlist = 0;
635 	inpara = 0;
636 	prefix = "";
637 	sharp = dosharp ? "#" : "";
638 	s = s_append(s, sharp);
639 	for(w=page; w; w=w->next){
640 		switch(w->type){
641 		case Wheading:
642 			if(inlist){
643 				prefix = "";
644 				inlist = 0;
645 			}
646 			s_endline(s, dosharp);
647 			if(!inpara){
648 				inpara = 1;
649 				s = s_appendlist(s, "\n", sharp, nil);
650 			}
651 			s = s_appendlist(s, w->text, "\n", sharp, "\n", sharp, nil);
652 			break;
653 
654 		case Wpara:
655 			s_endline(s, dosharp);
656 			if(inlist){
657 				prefix = "";
658 				inlist = 0;
659 			}
660 			if(!inpara){
661 				inpara = 1;
662 				s = s_appendlist(s, "\n", sharp, nil);
663 			}
664 			break;
665 
666 		case Wbullet:
667 			s_endline(s, dosharp);
668 			if(!inlist)
669 				inlist = 1;
670 			if(inpara)
671 				inpara = 0;
672 			s = s_append(s, " *\t");
673 			prefix = "\t";
674 			break;
675 
676 		case Wlink:
677 			if(inpara)
678 				inpara = 0;
679 			t = s_append(s_copy("["), w->text);
680 			if(w->url == nil)
681 				t = s_append(t, "]");
682 			else{
683 				t = s_append(t, " | ");
684 				t = s_append(t, w->url);
685 				t = s_append(t, "]");
686 			}
687 			s = s_appendbrk(s, s_to_c(t), prefix, dosharp);
688 			s_free(t);
689 			break;
690 
691 		case Wman:
692 			if(inpara)
693 				inpara = 0;
694 			s = s_appendbrk(s, w->text, prefix, dosharp);
695 			sprint(tmp, "(%d)", w->section);
696 			s = s_appendbrk(s, tmp, prefix, dosharp);
697 			break;
698 
699 		case Wpre:
700 			if(inlist){
701 				prefix = "";
702 				inlist = 0;
703 			}
704 			if(inpara)
705 				inpara = 0;
706 			s_endline(s, dosharp);
707 			s = s_appendlist(s, "! ", w->text, "\n", sharp, nil);
708 			break;
709 		case Whr:
710 			s_endline(s, dosharp);
711 			s = s_appendlist(s, "------------------------------------------------------ \n", sharp, nil);
712 			break;
713 
714 		case Wplain:
715 			if(inpara)
716 				inpara = 0;
717 			s = s_appendbrk(s, w->text, prefix, dosharp);
718 			break;
719 		}
720 	}
721 	s_endline(s, dosharp);
722 	s->ptr--;
723 	*s->ptr = '\0';
724 	return s;
725 }
726 
727 static String*
historytext(String * s,Whist * h)728 historytext(String *s, Whist *h)
729 {
730 	int i;
731 	char tmp[40];
732 	char *atime;
733 
734 	for(i=h->ndoc-1; i>=0; i--){
735 		if(i==h->current)
736 			sprint(tmp, "[current]");
737 		else
738 			sprint(tmp, "[%lud/]", h->doc[i].time);
739 		atime = ctime(h->doc[i].time);
740 		atime[strlen(atime)-1] = '\0';
741 		s = s_appendlist(s, " * ", tmp, " ", atime, nil);
742 		if(h->doc[i].author)
743 			s = s_appendlist(s, ", ", h->doc[i].author, nil);
744 		if(h->doc[i].conflict)
745 			s = s_append(s, ", conflicting write");
746 		s = s_append(s, "\n");
747 		if(h->doc[i].comment)
748 			s = s_appendlist(s, "<i>", h->doc[i].comment, "</i>\n", nil);
749 	}
750 	return s;
751 }
752 
753 String*
totext(Whist * h,Wdoc * d,int ty)754 totext(Whist *h, Wdoc *d, int ty)
755 {
756 	char *atime;
757 	char *p, *q, ver[40];
758 	int nsub;
759 	Sub sub[3];
760 	String *s, *t;
761 
762 	t = gettemplate(Ntemplate+ty);
763 	if(p = strstr(s_to_c(t), "PAGE"))
764 		q = p+4;
765 	else{
766 		p = s_to_c(t)+s_len(t);
767 		q = nil;
768 	}
769 
770 	nsub = 0;
771 	if(h){
772 		sub[nsub] = (Sub){ "TITLE", h->title };
773 		nsub++;
774 	}
775 	if(d){
776 		sprint(ver, "%lud", d->time);
777 		sub[nsub] = (Sub){ "VERSION", ver };
778 		nsub++;
779 		atime = ctime(d->time);
780 		atime[strlen(atime)-1] = '\0';
781 		sub[nsub] = (Sub){ "DATE", atime };
782 		nsub++;
783 	}
784 
785 	s = s_reset(nil);
786 	s = s_appendsub(s, s_to_c(t), p-s_to_c(t), sub, nsub);
787 	switch(ty){
788 	case Tpage:
789 	case Toldpage:
790 		s = pagetext(s, d->wtxt, 0);
791 		break;
792 	case Thistory:
793 		s = historytext(s, h);
794 		break;
795 	case Tnew:
796 	case Twerror:
797 		break;
798 	}
799 	if(q)
800 		s = s_appendsub(s, q, strlen(q), sub, nsub);
801 	s_free(t);
802 	return s;
803 }
804 
805 String*
doctext(String * s,Wdoc * d)806 doctext(String *s, Wdoc *d)
807 {
808 	char tmp[40];
809 
810 	sprint(tmp, "D%lud", d->time);
811 	s = s_append(s, tmp);
812 	if(d->comment){
813 		s = s_append(s, "\nC");
814 		s = s_append(s, d->comment);
815 	}
816 	if(d->author){
817 		s = s_append(s, "\nA");
818 		s = s_append(s, d->author);
819 	}
820 	if(d->conflict)
821 		s = s_append(s, "\nX");
822 	s = s_append(s, "\n");
823 	s = pagetext(s, d->wtxt, 1);
824 	return s;
825 }
826