1 #include <fcntl.h>
2 #include <errno.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <wctype.h>
6 #include <getopt.h>
7 #include <inttypes.h>
8 #include <sys/mman.h>
9 #include <sys/stat.h>
10 #include <sys/types.h>
11 #include <notcurses/notcurses.h>
12 #include "structure.h"
13 #include "builddef.h"
14 
15 static void
usage(const char * argv0,FILE * o)16 usage(const char* argv0, FILE* o){
17   fprintf(o, "usage: %s [ -hV ] files\n", argv0);
18   fprintf(o, " -h: print help and return success\n");
19   fprintf(o, " -v: print version and return success\n");
20 }
21 
22 static int
parse_args(int argc,char ** argv)23 parse_args(int argc, char** argv){
24   const char* argv0 = *argv;
25   int longindex;
26   int c;
27   struct option longopts[] = {
28     { .name = "help", .has_arg = 0, .flag = NULL, .val = 'h', },
29     { .name = NULL, .has_arg = 0, .flag = NULL, .val = 0, }
30   };
31   while((c = getopt_long(argc, argv, "hV", longopts, &longindex)) != -1){
32     switch(c){
33       case 'h': usage(argv0, stdout);
34                 exit(EXIT_SUCCESS);
35                 break;
36       case 'V': fprintf(stderr, "%s version %s\n", argv[0], notcurses_version());
37                 exit(EXIT_SUCCESS);
38                 break;
39       default: usage(argv0, stderr);
40                return -1;
41                break;
42     }
43   }
44   if(argv[optind] == NULL){
45     usage(argv0, stderr);
46     return -1;
47   }
48   return optind;
49 }
50 
51 #ifdef USE_DEFLATE // libdeflate implementation
52 #include <libdeflate.h>
53 // assume that |buf| is |*len| bytes of deflated data, and try to inflate
54 // it. if successful, the inflated map will be returned. either way, the
55 // input map will be unmapped (we take ownership). |*len| will be updated
56 // if an inflated map is successfully returned.
57 static unsigned char*
map_gzipped_data(unsigned char * buf,size_t * len,unsigned char * ubuf,uint32_t ulen)58 map_gzipped_data(unsigned char* buf, size_t* len, unsigned char* ubuf, uint32_t ulen){
59   struct libdeflate_decompressor* inflate = libdeflate_alloc_decompressor();
60   if(inflate == NULL){
61     fprintf(stderr, "couldn't get libdeflate inflator\n");
62     munmap(buf, *len);
63     return NULL;
64   }
65   size_t outbytes;
66   enum libdeflate_result r;
67   r = libdeflate_gzip_decompress(inflate, buf, *len, ubuf, ulen, &outbytes);
68   munmap(buf, *len);
69   libdeflate_free_decompressor(inflate);
70   if(r != LIBDEFLATE_SUCCESS){
71     fprintf(stderr, "error inflating %"PRIuPTR" (%d)\n", *len, r);
72     return NULL;
73   }
74   *len = ulen;
75   return ubuf;
76 }
77 #else // libz implementation
78 #include <zlib.h>
79 static unsigned char*
map_gzipped_data(unsigned char * buf,size_t * len,unsigned char * ubuf,uint32_t ulen)80 map_gzipped_data(unsigned char* buf, size_t* len, unsigned char* ubuf, uint32_t ulen){
81   z_stream z = {};
82   int r = inflateInit2(&z, 15 | 16);
83   if(r != Z_OK){
84     fprintf(stderr, "error getting zlib inflator (%d)\n", r);
85     munmap(buf, *len);
86     return NULL;
87   }
88   z.next_in = buf;
89   z.avail_in = *len;
90   z.next_out = ubuf;
91   z.avail_out = ulen;
92   r = inflate(&z, Z_FINISH);
93   munmap(buf, *len);
94   if(r != Z_STREAM_END){
95     fprintf(stderr, "error inflating (%d) (%s?)\n", r, z.msg);
96     inflateEnd(&z);
97     return NULL;
98   }
99   inflateEnd(&z);
100   munmap(buf, *len);
101   *len = ulen;
102   return ubuf;
103 }
104 #endif
105 
106 static unsigned char*
map_troff_data(int fd,size_t * len)107 map_troff_data(int fd, size_t* len){
108   struct stat sbuf;
109   if(fstat(fd, &sbuf)){
110     return NULL;
111   }
112   // gzip has a 10-byte mandatory header and an 8-byte mandatory footer
113   if(sbuf.st_size < 18){
114     return NULL;
115   }
116   *len = sbuf.st_size;
117   unsigned char* buf = mmap(NULL, *len, PROT_READ,
118 #ifdef MAP_POPULATE
119                             MAP_POPULATE |
120 #endif
121                             MAP_PRIVATE, fd, 0);
122   if(buf == MAP_FAILED){
123     fprintf(stderr, "error mapping %"PRIuPTR" (%s?)\n", *len, strerror(errno));
124     return NULL;
125   }
126   if(buf[0] == 0x1f && buf[1] == 0x8b && buf[2] == 0x08){
127     // the last four bytes have the uncompressed length
128     uint32_t ulen;
129     memcpy(&ulen, buf + *len - 4, 4);
130     long sc = sysconf(_SC_PAGESIZE);
131     if(sc <= 0){
132       fprintf(stderr, "couldn't get page size\n");
133       return NULL;
134     }
135     size_t pgsize = sc;
136     void* ubuf = mmap(NULL, (ulen + pgsize - 1) / pgsize * pgsize,
137                       PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
138     if(ubuf == MAP_FAILED){
139       fprintf(stderr, "error mapping %"PRIu32" (%s?)\n", ulen, strerror(errno));
140       munmap(buf, *len);
141       return NULL;
142     }
143     if(map_gzipped_data(buf, len, ubuf, ulen) == NULL){
144       munmap(ubuf, ulen);
145       return NULL;
146     }
147     return ubuf;
148   }
149   return buf;
150 }
151 
152 // find the man page, and inflate it if deflated
153 static unsigned char*
get_troff_data(const char * arg,size_t * len)154 get_troff_data(const char *arg, size_t* len){
155   // FIXME we'll want to use the mandb. for now, require a full path.
156   int fd = open(arg, O_RDONLY | O_CLOEXEC);
157   if(fd < 0){
158     fprintf(stderr, "error opening %s (%s?)\n", arg, strerror(errno));
159     return NULL;
160   }
161   unsigned char* buf = map_troff_data(fd, len);
162   close(fd);
163   return buf;
164 }
165 
166 typedef enum {
167   LINE_UNKNOWN,
168   LINE_COMMENT,
169   LINE_B, LINE_BI, LINE_BR, LINE_I, LINE_IB, LINE_IR,
170   LINE_RB, LINE_RI, LINE_SB, LINE_SM,
171   LINE_EE, LINE_EX, LINE_RE, LINE_RS,
172   LINE_SH, LINE_SS, LINE_TH,
173   LINE_IP, LINE_LP, LINE_P, LINE_PP,
174   LINE_TP, LINE_TQ,
175   LINE_ME, LINE_MT, LINE_UE, LINE_UR,
176   LINE_OP, LINE_SY, LINE_YS,
177   LINE_NF, LINE_FI,
178 } ltypes;
179 
180 typedef enum {
181   TROFF_UNKNOWN,
182   TROFF_COMMENT,
183   TROFF_FONT,
184   TROFF_STRUCTURE,
185   TROFF_PARAGRAPH,
186   TROFF_HYPERLINK,
187   TROFF_SYNOPSIS,
188   TROFF_PREFORMATTED,
189 } ttypes;
190 
191 typedef struct {
192   ltypes ltype;
193   const char* symbol;
194   ttypes ttype;
195   uint32_t channel;
196 } trofftype;
197 
198 // all troff types start with a period, followed by one or two ASCII
199 // characters.
200 static const trofftype trofftypes[] = {
201   { .ltype = LINE_UNKNOWN, .symbol = "", .ttype = TROFF_UNKNOWN, .channel = 0, },
202   { .ltype = LINE_COMMENT, .symbol = "\\\"", .ttype = TROFF_COMMENT, .channel = 0, },
203 #define TROFF_FONT(x) { .ltype = LINE_##x, .symbol = #x, .ttype = TROFF_FONT, .channel = 0, },
204   TROFF_FONT(B) TROFF_FONT(BI) TROFF_FONT(BR)
205   TROFF_FONT(I) TROFF_FONT(IB) TROFF_FONT(IR)
206 #undef TROFF_FONT
207 #define TROFF_STRUCTURE(x, c) { .ltype = LINE_##x, .symbol = #x, .ttype = TROFF_STRUCTURE, .channel = (c), },
208   TROFF_STRUCTURE(EE, 0)
209   TROFF_STRUCTURE(EX, 0)
210   TROFF_STRUCTURE(RE, 0)
211   TROFF_STRUCTURE(RS, 0)
212   TROFF_STRUCTURE(SH, NCCHANNEL_INITIALIZER(0x9b, 0x9b, 0xfc))
213   TROFF_STRUCTURE(SS, NCCHANNEL_INITIALIZER(0x6c, 0x6b, 0xfb))
214   TROFF_STRUCTURE(TH, NCCHANNEL_INITIALIZER(0xcb, 0xcb, 0xfd))
215 #undef TROFF_STRUCTURE
216 #define TROFF_PARA(x) { .ltype = LINE_##x, .symbol = #x, .ttype = TROFF_PARAGRAPH, .channel = 0, },
217   TROFF_PARA(IP) TROFF_PARA(LP) TROFF_PARA(P)
218   TROFF_PARA(PP) TROFF_PARA(TP) TROFF_PARA(TQ)
219 #undef TROFF_PARA
220 #define TROFF_HLINK(x) { .ltype = LINE_##x, .symbol = #x, .ttype = TROFF_HYPERLINK, .channel = 0, },
221   TROFF_HLINK(ME) TROFF_HLINK(MT) TROFF_HLINK(UE) TROFF_HLINK(UR)
222 #undef TROFF_HLINK
223 #define TROFF_SYNOPSIS(x) { .ltype = LINE_##x, .symbol = #x, .ttype = TROFF_SYNOPSIS, .channel = 0, },
224   TROFF_SYNOPSIS(OP) TROFF_SYNOPSIS(SY) TROFF_SYNOPSIS(YS)
225 #undef TROFF_SYNOPSIS
226   { .ltype = LINE_UNKNOWN, .symbol = "hy", .ttype = TROFF_UNKNOWN, .channel = 0, },
227   { .ltype = LINE_UNKNOWN, .symbol = "br", .ttype = TROFF_UNKNOWN, .channel = 0, },
228   { .ltype = LINE_COMMENT, .symbol = "IX", .ttype = TROFF_COMMENT, .channel = 0, },
229   { .ltype = LINE_NF, .symbol = "nf", .ttype = TROFF_FONT, .channel = 0, },
230   { .ltype = LINE_FI, .symbol = "fi", .ttype = TROFF_FONT, .channel = 0, },
231 };
232 
233 // the troff trie is only defined on the 128 ascii values.
234 struct troffnode {
235   struct troffnode* next[0x80];
236   const trofftype *ttype;
237 };
238 
239 static void
destroy_trofftrie(struct troffnode * root)240 destroy_trofftrie(struct troffnode* root){
241   if(root){
242     for(unsigned i = 0 ; i < sizeof(root->next) / sizeof(*root->next) ; ++i){
243       destroy_trofftrie(root->next[i]);
244     }
245     free(root);
246   }
247 }
248 
249 // build a trie rooted at an implicit leading period.
250 static struct troffnode*
trofftrie(void)251 trofftrie(void){
252   struct troffnode* root = malloc(sizeof(*root));
253   if(root == NULL){
254     return NULL;
255   }
256   memset(root, 0, sizeof(*root));
257   for(size_t toff = 0 ; toff < sizeof(trofftypes) / sizeof(*trofftypes) ; ++toff){
258     const trofftype* t = &trofftypes[toff];
259     if(strlen(t->symbol) == 0){
260       continue;
261     }
262     struct troffnode* n = root;
263     for(const char* s = t->symbol ; *s ; ++s){
264       if(*s < 0){ // illegal symbol
265         fprintf(stderr, "illegal symbol: %s\n", t->symbol);
266         goto err;
267       }
268       unsigned char us = *s;
269       if(us > sizeof(root->next) / sizeof(*root->next)){ // illegal symbol
270         fprintf(stderr, "illegal symbol: %s\n", t->symbol);
271         goto err;
272       }
273       if(n->next[us] == NULL){
274         if((n->next[us] = malloc(sizeof(*root))) == NULL){
275           goto err;
276         }
277         memset(n->next[us], 0, sizeof(*root));
278       }
279       n = n->next[us];
280     }
281     if(n->ttype){ // duplicate command
282       fprintf(stderr, "duplicate command: %s %s\n", t->symbol, n->ttype->symbol);
283       goto err;
284     }
285     n->ttype = t;
286   }
287   return root;
288 
289 err:
290   destroy_trofftrie(root);
291   return NULL;
292 }
293 
294 // lex the troffnode out from |ws|, where the troffnode is all text prior to
295 // whitespace or a NUL. the byte following the troffnode is written back to
296 // |ws|. if it is a valid troff command sequence, the node is returned;
297 // NULL is otherwise returned. |len| ought be non-negative.
298 static const trofftype*
get_type(const struct troffnode * trie,const unsigned char ** ws,size_t len)299 get_type(const struct troffnode* trie, const unsigned char** ws, size_t len){
300   if(**ws != '.'){
301     return NULL;
302   }
303   ++*ws;
304   --len;
305   while(len && !isspace(**ws) && **ws){
306     if(**ws > sizeof(trie->next) / sizeof(*trie->next)){ // illegal command
307       return NULL;
308     }
309     if((trie = trie->next[**ws]) == NULL){
310       return NULL;
311     }
312     ++*ws;
313     --len;
314   }
315   return trie->ttype;
316 }
317 
318 typedef struct pagenode {
319   char* text;
320   const trofftype* ttype;
321   struct pagenode* subs;
322   unsigned subcount;
323 } pagenode;
324 
325 typedef struct pagedom {
326   struct pagenode* root;
327   struct troffnode* trie;
328   char* title;
329   char* section;
330   char* version;
331   char* footer;
332   char* header;
333   struct docstructure* ds;
334 } pagedom;
335 
336 static const char*
dom_get_title(const pagedom * dom)337 dom_get_title(const pagedom* dom){
338   return dom->title;
339 }
340 
341 // get the next token. first, chew whitespace. then match a string of
342 // iswgraph(), or a quoted string of iswprint(). return the number of
343 // characters consumed, or -1 on error (no token, unterminated quote).
344 // heap-copies the utf8 to *token on success.
345 static int
lex_next_token(const char * s,char ** token)346 lex_next_token(const char* s, char** token){
347   mbstate_t ps = {};
348   wchar_t w;
349   size_t b, cur;
350   cur = 0;
351   bool inquote = false;
352   const char* tokstart = NULL;
353   while((b = mbrtowc(&w, s + cur, MB_CUR_MAX, &ps)) != (size_t)-1 && b != (size_t)-2){
354     if(tokstart){
355       if(b == 0 || (inquote && w == L'"') || (!inquote && iswspace(w))){
356         if(!tokstart || !*tokstart || *tokstart == '"'){
357           return -1;
358         }
359         *token = strndup(tokstart, cur - (tokstart - s));
360         return cur + b;
361       }
362     }else{
363       if(iswspace(w)){
364         cur += b;
365         continue;
366       }
367       if(w == '"'){
368         inquote = true;
369         tokstart = s + cur + b;
370       }else{
371         tokstart = s + cur;
372       }
373     }
374     cur += b;
375   }
376   return -1;
377 }
378 
379 // take the newly-added title section, and extract the title, section, and
380 // version (technically footer-middle, footer-inside, and header-middle).
381 // they ought be quoted, but might not be.
382 static int
lex_title(pagedom * dom)383 lex_title(pagedom* dom){
384   const char* tok = dom->root->text;
385   int b = lex_next_token(tok, &dom->title);
386   if(b < 0){
387     fprintf(stderr, "couldn't extract title [%s]\n", dom->root->text);
388     return -1;
389   }
390   tok += b;
391   b = lex_next_token(tok, &dom->section);
392   if(b < 0){
393     fprintf(stderr, "couldn't extract section [%s]\n", dom->root->text);
394     return -1;
395   }
396   tok += b;
397   b = lex_next_token(tok, &dom->version);
398   if(b < 0){
399     //fprintf(stderr, "couldn't extract version [%s]\n", dom->root->text);
400     return 0;
401   }
402   tok += b;
403   b = lex_next_token(tok, &dom->footer);
404   if(b < 0){
405     //fprintf(stderr, "couldn't extract footer [%s]\n", dom->root->text);
406     return 0;
407   }
408   tok += b;
409   b = lex_next_token(tok, &dom->header);
410   if(b < 0){
411     //fprintf(stderr, "couldn't extract header [%s]\n", dom->root->text);
412     return 0;
413   }
414   return 0;
415 }
416 
417 static pagenode*
add_node(pagenode * pnode,char * text)418 add_node(pagenode* pnode, char* text){
419   unsigned ncount = pnode->subcount + 1;
420   pagenode* tmpsubs = realloc(pnode->subs, sizeof(*pnode->subs) * ncount);
421   if(tmpsubs == NULL){
422     return NULL;
423   }
424   pnode->subs = tmpsubs;
425   pagenode* r = pnode->subs + pnode->subcount;
426   pnode->subcount = ncount;
427   memset(r, 0, sizeof(*r));
428   r->text = text;
429 //fprintf(stderr, "ADDED SECTION %s %u\n", text, pnode->subcount);
430   return r;
431 }
432 
433 static char*
extract_text(const unsigned char * ws,const unsigned char * feol)434 extract_text(const unsigned char* ws, const unsigned char* feol){
435   if(ws == feol || ws == feol + 1){
436     fprintf(stderr, "bogus empty title\n");
437     return NULL;
438   }
439   return strndup((const char*)ws + 1, feol - ws);
440 }
441 
442 static char*
augment_text(pagenode * pnode,const unsigned char * ws,const unsigned char * feol)443 augment_text(pagenode* pnode, const unsigned char* ws, const unsigned char* feol){
444   const size_t slen = pnode->text ? strlen(pnode->text) + 1 : 0;
445   char* tmp = realloc(pnode->text, slen + (feol - ws) + 2);
446   if(tmp == NULL){
447     return NULL;
448   }
449   pnode->text = tmp;
450   if(slen){
451     pnode->text[slen - 1] = ' ';
452   }
453   memcpy(pnode->text + slen, ws, feol - ws + 1);
454   pnode->text[slen + (feol - ws + 1)] = '\0';
455   return pnode->text;
456 }
457 
458 // extract the page structure.
459 // FIXME we need to fuzz this, hard
460 static int
troff_parse(const unsigned char * map,size_t mlen,pagedom * dom)461 troff_parse(const unsigned char* map, size_t mlen, pagedom* dom){
462   const struct troffnode* trie = dom->trie;
463   const unsigned char* line = map;
464   pagenode* current_section = NULL;
465   pagenode* current_subsection = NULL;
466   pagenode* current_para = NULL;
467   bool preformatted = false;
468   for(size_t off = 0 ; off < mlen ; ++off){
469     const unsigned char* ws = line;
470     size_t left = mlen - off;
471     const trofftype* node = get_type(trie, &ws, left);
472     // find the end of this line
473     const unsigned char* eol = ws;
474     left -= (ws - line);
475     while(left && *eol != '\n' && *eol){
476       ++eol;
477       --left;
478     }
479     const unsigned char* feol = eol;
480     // functional end of line--doesn't include possible newline
481     if(!preformatted){
482       if(left && *eol == '\n'){
483         --feol;
484       }
485     }
486     if(node == NULL){
487       if(current_para == NULL){
488         //fprintf(stderr, "free-floating text transcends para\n");
489         //fprintf(stderr, "[%s]\n", line);
490       }else{
491         char* et = augment_text(current_para, line, feol);
492         if(et == NULL){
493           return -1;
494         }
495       }
496     }else if(node->ltype == LINE_NF){
497       preformatted = true;
498     }else if(node->ltype == LINE_FI){
499       preformatted = false;
500     }else if(node->ltype == LINE_TH){
501       if(dom_get_title(dom)){
502         fprintf(stderr, "found a second title (was %s)\n", dom_get_title(dom));
503         return -1;
504       }
505       char* et = extract_text(ws, feol);
506       if(et == NULL){
507         return -1;
508       }
509       if((dom->root = malloc(sizeof(*dom->root))) == NULL){
510         free(et);
511         return -1;
512       }
513       memset(dom->root, 0, sizeof(*dom->root));
514       dom->root->ttype = node;
515       dom->root->text = et;
516       if(lex_title(dom)){
517         return -1;
518       }
519       current_para = dom->root;
520     }else if(node->ltype == LINE_SH){
521       if(dom->root == NULL){
522         fprintf(stderr, "section transcends structure\n");
523         return -1;
524       }
525       char* et = extract_text(ws, feol);
526       if(et == NULL){
527         return -1;
528       }
529       if((current_section = add_node(dom->root, et)) == NULL){
530         free(et);
531         return -1;
532       }
533       current_section->ttype = node;
534       current_subsection = NULL;
535       current_para = current_section;
536     }else if(node->ltype == LINE_SS){
537       char* et = extract_text(ws, feol);
538       if(et == NULL){
539         return -1;
540       }
541       if(current_section == NULL){
542         fprintf(stderr, "subsection %s without section\n", et);
543         free(et);
544         return -1;
545       }
546       if((current_subsection = add_node(current_section, et)) == NULL){
547         free(et);
548         return -1;
549       }
550       current_subsection->ttype = node;
551       current_para = current_subsection;
552     }else if(node->ltype == LINE_PP){
553       if(dom->root == NULL){
554         fprintf(stderr, "paragraph transcends structure\n");
555         return -1;
556       }
557       if((current_para = add_node(current_para, NULL)) == NULL){
558         return -1;
559       }
560       current_para->ttype = node;
561     }else if(node->ltype == LINE_TP){
562       if(dom->root == NULL){
563         fprintf(stderr, "tagged paragraph transcends structure\n");
564         return -1;
565       }
566       if((current_para = add_node(current_para, NULL)) == NULL){
567         return -1;
568       }
569       current_para->ttype = node;
570     }
571     off += eol - line;
572     line = eol + 1;
573   }
574   if(dom_get_title(dom) == NULL){
575     fprintf(stderr, "no title found\n");
576     return -1;
577   }
578   return 0;
579 }
580 
581 // invoke ncplane_puttext() on the text starting at s and ending
582 // (non-inclusive) at e.
583 static int
puttext(struct ncplane * p,const char * s,const char * e)584 puttext(struct ncplane* p, const char* s, const char* e){
585   if(e <= s){
586 //fprintf(stderr, "no text to print\n");
587     return 0; // no text to print
588   }
589   char* dup = strndup(s, e - s);
590   if(dup == NULL){
591     return -1;
592   }
593   size_t b = 0;
594   int r = ncplane_puttext(p, -1, NCALIGN_LEFT, dup, &b);
595   free(dup);
596   return r;
597 }
598 
599 // paragraphs can have formatting information inline within their text. we
600 // proceed until we find such an inline marker, print the text we've skipped,
601 // set up the style, and continue.
602 static int
putpara(struct ncplane * p,const char * text)603 putpara(struct ncplane* p, const char* text){
604   // cur indicates where the current text to be displayed starts.
605   const char* cur = text;
606   uint16_t style = 0;
607   const char* posttext = NULL;
608   while(*cur){
609     // find the next style marker
610     bool inescape = false;
611     const char* textend = NULL; // one past where the text to print ends
612     // curend is where the text + style markings end
613     const char* curend;
614     for(curend = cur ; *curend ; ++curend){
615       if(*curend == '\\'){
616         if(inescape){ // escaped backslash
617           inescape = false;
618         }else{
619           inescape = true;
620         }
621       }else if(inescape){
622         if(*curend == 'f'){ // set font
623           textend = curend - 1; // account for backslash
624           bool bracketed = false;
625           if(*++curend == '['){
626             bracketed = true;
627             ++curend;
628           }
629           while(isalpha(*curend)){
630             switch(toupper(*curend)){
631               case 'R': style = 0; break; // roman, default
632               case 'I': style |= NCSTYLE_ITALIC; break;
633               case 'B': style |= NCSTYLE_BOLD; break;
634               case 'C': break; // unsure! seems to be used with .nf/.fi
635               default:
636                 fprintf(stderr, "unknown font macro %s\n", curend);
637                 return -1;
638             }
639             ++curend;
640           }
641           if(bracketed){
642             if(*curend != ']'){
643               fprintf(stderr, "missing ']': %s\n", curend);
644               return -1;
645             }
646             ++curend;
647           }
648           break;
649         }else if(*curend == '['){ // escaped sequence
650           textend = curend - 1; // account for backslash
651           static const struct {
652             const char* macro;
653             const char* tr;
654           } macros[] = {
655             { "aq]", "'", },
656             { "dq]", "\"", },
657             { "lq]", u8"\u201c", }, // left double quote
658             { "rq]", u8"\u201d", }, // right double quote
659             { "em]", u8"\u2014", }, // em dash
660             { "en]", u8"\u2013", }, // en dash
661             { "rg]", "®", },
662             { "rs]", "\\", },
663             { "ti]", "~", },
664             { NULL, NULL, }
665           };
666           ++curend;
667           const char* macend = NULL;
668           for(typeof(&*macros) m = macros ; m->tr ; ++m){
669             if(strncmp(curend, m->macro, strlen(m->macro)) == 0){
670               posttext = m->tr;
671               macend = curend + strlen(m->macro);
672               break;
673             }
674           }
675           if(macend == NULL){
676             fprintf(stderr, "unknown macro %s\n", curend);
677             return -1;
678           }
679           curend = macend;
680           break;
681         }else{
682           inescape = false;
683           cur = curend++;
684           break;
685         }
686         inescape = false;
687       }
688     }
689     if(textend == NULL){
690       textend = curend;
691     }
692     if(puttext(p, cur, textend) < 0){
693       return -1;
694     }
695     if(posttext){
696       if(puttext(p, posttext, posttext + strlen(posttext)) < 0){
697         return -1;
698       }
699       posttext = NULL;
700     }
701     cur = curend;
702     ncplane_set_styles(p, style);
703   }
704   ncplane_cursor_move_yx(p, -1, 0);
705   return 0;
706 }
707 
708 static int
draw_domnode(struct ncplane * p,const pagedom * dom,const pagenode * n,unsigned * wrotetext)709 draw_domnode(struct ncplane* p, const pagedom* dom, const pagenode* n,
710              unsigned* wrotetext){
711   ncplane_set_fchannel(p, n->ttype->channel);
712   size_t b = 0;
713   switch(n->ttype->ltype){
714     case LINE_TH:
715       if(docstructure_add(dom->ds, dom->title, ncplane_y(p), DOCSTRUCTURE_TITLE)){
716         return -1;
717       }
718       /*
719       ncplane_set_styles(p, NCSTYLE_UNDERLINE);
720       ncplane_printf_aligned(p, 0, NCALIGN_LEFT, "%s(%s)", dom->title, dom->section);
721       ncplane_printf_aligned(p, 0, NCALIGN_RIGHT, "%s(%s)", dom->title, dom->section);
722       ncplane_set_styles(p, NCSTYLE_NONE);
723       */break;
724     case LINE_SH: // section heading
725       if(docstructure_add(dom->ds, dom->title, ncplane_y(p), DOCSTRUCTURE_SECTION)){
726         return -1;
727       }
728       if(strcmp(n->text, "NAME")){
729         ncplane_puttext(p, -1, NCALIGN_LEFT, "\n\n", &b);
730         ncplane_set_styles(p, NCSTYLE_BOLD | NCSTYLE_UNDERLINE);
731         ncplane_putstr_aligned(p, -1, NCALIGN_CENTER, n->text);
732         ncplane_set_styles(p, NCSTYLE_NONE);
733         ncplane_cursor_move_yx(p, -1, 0);
734         *wrotetext = true;
735       }
736       break;
737     case LINE_SS: // subsection heading
738       if(docstructure_add(dom->ds, dom->title, ncplane_y(p), DOCSTRUCTURE_SUBSECTION)){
739         return -1;
740       }
741       ncplane_puttext(p, -1, NCALIGN_LEFT, "\n\n", &b);
742       ncplane_set_styles(p, NCSTYLE_ITALIC | NCSTYLE_UNDERLINE);
743       ncplane_putstr_aligned(p, -1, NCALIGN_CENTER, n->text);
744       ncplane_set_styles(p, NCSTYLE_NONE);
745       ncplane_cursor_move_yx(p, -1, 0);
746       *wrotetext = true;
747       break;
748     case LINE_PP: // paragraph
749     case LINE_TP: // tagged paragraph
750     case LINE_IP: // indented paragraph
751       if(*wrotetext){
752         if(n->text){
753           ncplane_puttext(p, -1, NCALIGN_LEFT, "\n\n", &b);
754           putpara(p, n->text);
755         }
756       }else{
757         ncplane_set_styles(p, NCSTYLE_BOLD | NCSTYLE_ITALIC);
758         ncplane_set_fg_rgb(p, 0xff6a00);
759         ncplane_putstr_aligned(p, -1, NCALIGN_CENTER, n->text);
760         ncplane_set_fg_default(p);
761         ncplane_set_styles(p, NCSTYLE_NONE);
762       }
763       *wrotetext = true;
764       break;
765     default:
766       fprintf(stderr, "unhandled ltype %d\n", n->ttype->ltype);
767       return 0; // FIXME
768   }
769   for(unsigned z = 0 ; z < n->subcount ; ++z){
770     if(draw_domnode(p, dom, &n->subs[z], wrotetext)){
771       return -1;
772     }
773   }
774   return 0;
775 }
776 
777 // for now, we draw the entire thing, resizing as necessary, and we'll
778 // scroll the entire plane. higher memory cost, longer initial latency,
779 // very fast moves.
780 static int
draw_content(struct ncplane * stdn,struct ncplane * p)781 draw_content(struct ncplane* stdn, struct ncplane* p){
782   pagedom* dom = ncplane_userptr(p);
783   unsigned wrotetext = 0; // unused by us
784   dom->ds = docstructure_create(stdn);
785   if(dom->ds == NULL){
786     return -1;
787   }
788   return draw_domnode(p, dom, dom->root, &wrotetext);
789 }
790 
791 static int
resize_pman(struct ncplane * pman)792 resize_pman(struct ncplane* pman){
793   unsigned dimy, dimx;
794   ncplane_dim_yx(ncplane_parent_const(pman), &dimy, &dimx);
795   ncplane_resize_simple(pman, dimy - 1, dimx);
796   struct ncplane* stdn = notcurses_stdplane(ncplane_notcurses(pman));
797   int r = draw_content(stdn, pman);
798   ncplane_move_yx(pman, 0, 0);
799   return r;
800 }
801 
802 // we create a plane sized appropriately for the troff data. all we do
803 // after that is move the plane up and down.
804 static struct ncplane*
render_troff(struct notcurses * nc,const unsigned char * map,size_t mlen,pagedom * dom)805 render_troff(struct notcurses* nc, const unsigned char* map, size_t mlen,
806              pagedom* dom){
807   unsigned dimy, dimx;
808   struct ncplane* stdn = notcurses_stddim_yx(nc, &dimy, &dimx);
809   // this is just an estimate
810   if(troff_parse(map, mlen, dom)){
811     return NULL;
812   }
813   // this is just an estimate
814   struct ncplane_options popts = {
815     .rows = dimy - 1,
816     .cols = dimx,
817     .userptr = dom,
818     .resizecb = resize_pman,
819     .flags = NCPLANE_OPTION_AUTOGROW | NCPLANE_OPTION_VSCROLL,
820   };
821   struct ncplane* pman = ncplane_create(stdn, &popts);
822   if(pman == NULL){
823     return NULL;
824   }
825   if(draw_content(stdn, pman)){
826     ncplane_destroy(pman);
827     return NULL;
828   }
829   return pman;
830 }
831 
832 static const char USAGE_TEXT[] = "⎥b⇞k↑j↓f⇟⎢ (q)uit";
833 static const char USAGE_TEXT_ASCII[] = "(bkjf) (q)uit";
834 
835 static int
draw_bar(struct ncplane * bar,pagedom * dom)836 draw_bar(struct ncplane* bar, pagedom* dom){
837   ncplane_cursor_move_yx(bar, 0, 0);
838   ncplane_set_styles(bar, NCSTYLE_BOLD);
839   ncplane_putstr(bar, dom_get_title(dom));
840   ncplane_set_styles(bar, NCSTYLE_NONE);
841   ncplane_putchar(bar, '(');
842   ncplane_set_styles(bar, NCSTYLE_BOLD);
843   ncplane_putstr(bar, dom->section);
844   ncplane_set_styles(bar, NCSTYLE_NONE);
845   ncplane_printf(bar, ") %s", dom->version);
846   ncplane_set_styles(bar, NCSTYLE_ITALIC);
847   if(notcurses_canutf8(ncplane_notcurses(bar))){
848     ncplane_putstr_aligned(bar, 0, NCALIGN_RIGHT, USAGE_TEXT);
849   }else{
850     ncplane_putstr_aligned(bar, 0, NCALIGN_RIGHT, USAGE_TEXT_ASCII);
851   }
852   return 0;
853 }
854 
855 static int
resize_bar(struct ncplane * bar)856 resize_bar(struct ncplane* bar){
857   unsigned dimy, dimx;
858   ncplane_dim_yx(ncplane_parent_const(bar), &dimy, &dimx);
859   ncplane_resize_simple(bar, 1, dimx);
860   int r = draw_bar(bar, ncplane_userptr(bar));
861   ncplane_move_yx(bar, dimy - 1, 0);
862   return r;
863 }
864 
865 static void
domnode_destroy(pagenode * node)866 domnode_destroy(pagenode* node){
867   if(node){
868     free(node->text);
869     for(unsigned z = 0 ; z < node->subcount ; ++z){
870       domnode_destroy(&node->subs[z]);
871     }
872     free(node->subs);
873   }
874 }
875 
876 static void
pagedom_destroy(pagedom * dom)877 pagedom_destroy(pagedom* dom){
878   destroy_trofftrie(dom->trie);
879   domnode_destroy(dom->root);
880   free(dom->root);
881   free(dom->title);
882   free(dom->version);
883   free(dom->section);
884   docstructure_free(dom->ds);
885 }
886 
887 static struct ncplane*
create_bar(struct notcurses * nc,pagedom * dom)888 create_bar(struct notcurses* nc, pagedom* dom){
889   unsigned dimy, dimx;
890   struct ncplane* stdn = notcurses_stddim_yx(nc, &dimy, &dimx);
891   struct ncplane_options nopts = {
892     .y = dimy - 1,
893     .x = 0,
894     .rows = 1,
895     .cols = dimx,
896     .resizecb = resize_bar,
897     .userptr = dom,
898   };
899   struct ncplane* bar = ncplane_create(stdn, &nopts);
900   if(bar == NULL){
901     return NULL;
902   }
903   uint64_t barchan = NCCHANNELS_INITIALIZER(0, 0, 0, 0x26, 0x62, 0x41);
904   ncplane_set_fg_rgb(bar, 0xffffff);
905   if(ncplane_set_base(bar, " ", 0, barchan) != 1){
906     ncplane_destroy(bar);
907     return NULL;
908   }
909   if(draw_bar(bar, dom)){
910     ncplane_destroy(bar);
911     return NULL;
912   }
913   if(notcurses_render(nc)){
914     ncplane_destroy(bar);
915     return NULL;
916   }
917   return bar;
918 }
919 
920 static int
manloop(struct notcurses * nc,const char * arg)921 manloop(struct notcurses* nc, const char* arg){
922   struct ncplane* stdn = notcurses_stdplane(nc);
923   int ret = -1;
924   struct ncplane* page = NULL;
925   struct ncplane* bar = NULL;
926   pagedom dom = {};
927   size_t len;
928   unsigned char* buf = get_troff_data(arg, &len);
929   if(buf == NULL){
930     goto done;
931   }
932   dom.trie = trofftrie();
933   if(dom.trie == NULL){
934     goto done;
935   }
936   page = render_troff(nc, buf, len, &dom);
937   if(page == NULL){
938     goto done;
939   }
940   bar = create_bar(nc, &dom);
941   if(bar == NULL){
942     goto done;
943   }
944   uint32_t key;
945   do{
946     if(notcurses_render(nc)){
947       goto done;
948     }
949     ncinput ni;
950     key = notcurses_get(nc, NULL, &ni);
951     if(ni.evtype == NCTYPE_RELEASE){
952       continue;
953     }
954     switch(key){
955       case 'L':
956         if(ni.ctrl && !ni.alt){
957           notcurses_refresh(nc, NULL, NULL);
958         }
959         break;
960       case 'k': case NCKEY_UP:
961         if(ncplane_y(page)){
962           ncplane_move_rel(page, 1, 0);
963         }
964         break;
965       // we can move down iff our last line is beyond the visible area
966       case 'j': case NCKEY_DOWN:
967         if(ncplane_y(page) + ncplane_dim_y(page) >= ncplane_dim_y(stdn)){
968           ncplane_move_rel(page, -1, 0);
969         }
970         break;
971       case 'b': case NCKEY_PGUP:{
972         int newy = ncplane_y(page) + (int)ncplane_dim_y(stdn);
973         if(newy > 0){
974           newy = 0;
975         }
976         ncplane_move_yx(page, newy, 0);
977         break;
978       }case 'f': case NCKEY_PGDOWN:{
979         int newy = ncplane_y(page) - (int)ncplane_dim_y(stdn) + 1;
980         if(newy + (int)ncplane_dim_y(page) < (int)ncplane_dim_y(stdn)){
981           newy += (int)ncplane_dim_y(stdn) - (newy + (int)ncplane_dim_y(page)) - 1;
982         }
983         ncplane_move_yx(page, newy, 0);
984         break;
985       }case 'q':
986         ret = 0;
987         goto done;
988     }
989   }while(key != (uint32_t)-1);
990 
991 done:
992   if(page){
993     ncplane_destroy(page);
994   }
995   ncplane_destroy(bar);
996   if(buf){
997     munmap(buf, len);
998   }
999   pagedom_destroy(&dom);
1000   return ret;
1001 }
1002 
1003 static int
tfman(struct notcurses * nc,const char * arg)1004 tfman(struct notcurses* nc, const char* arg){
1005   int r = manloop(nc, arg);
1006   return r;
1007 }
1008 
main(int argc,char ** argv)1009 int main(int argc, char** argv){
1010   int nonopt = parse_args(argc, argv);
1011   if(nonopt <= 0){
1012     return EXIT_FAILURE;
1013   }
1014   struct notcurses_options nopts = {
1015   };
1016   struct notcurses* nc = notcurses_core_init(&nopts, NULL);
1017   if(nc == NULL){
1018     return EXIT_FAILURE;
1019   }
1020   bool success;
1021   for(int i = 0 ; i < argc - nonopt ; ++i){
1022     success = false;
1023     if(tfman(nc, argv[nonopt + i])){
1024       break;
1025     }
1026     success = true;
1027   }
1028   return notcurses_stop(nc) || !success ? EXIT_FAILURE : EXIT_SUCCESS;
1029 }
1030