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