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