1 #include "config.h"
2 #include <X11/Xatom.h>
3 #include "ylistbox.h"
4 #include "ymenu.h"
5 #include "yxapp.h"
6 #include "sysdep.h"
7 #include "yaction.h"
8 #include "yinputline.h"
9 #include "ymenuitem.h"
10 #include "ylocale.h"
11 #include "yrect.h"
12 #include "ascii.h"
13 #include "intl.h"
14 #include "ykey.h"
15 #include "yfontname.h"
16 #include "yfontbase.h"
17 #include <strings.h>
18 #include <X11/Xutil.h>
19
20 #define ICEWM_SITE "https://ice-wm.org/"
21 #define ICEWM_FAQ "https://ice-wm.org/FAQ/"
22 #define THEME_HOWTO "https://ice-wm.org/themes/"
23 #define ICEGIT_SITE "https://github.com/bbidulock/icewm/"
24 #define ICEWM_1 DOCDIR "/icewm.1.html"
25 #define ICEWMBG_1 DOCDIR "/icewmbg.1.html"
26 #define ICESOUND_1 DOCDIR "/icesound.1.html"
27 #define BACKGROUND "rgb:E0/E0/E0"
28 #define FOREGROUND "rgb:00/00/00"
29 #define LINK_COLOR "rgb:06/45/AD"
30 #define RULE_COLOR "rgb:80/80/80"
31
32 #ifdef DEBUG
33 #define DUMP
34 //#define TEXT
35 #endif
36
37 #define SPACE(c) ASCII::isWhiteSpace(c)
38
39 enum ViewerDimensions {
40 ViewerWidth = 700,
41 ViewerHeight = 700,
42 ViewerLeftMargin = 20,
43 ViewerTopMargin = 10,
44 };
45
46 char const * ApplicationName = "icehelp";
47
48 static bool verbose, complain, ignoreClose, noFreeType, reverseVideo;
49
50 class cbuffer {
51 size_t cap, ins;
52 char *ptr;
53 public:
cbuffer()54 cbuffer() : cap(0), ins(0), ptr(nullptr) {
55 }
cbuffer(const char * s)56 cbuffer(const char *s) : cap(1 + strlen(s)), ins(cap - 1),
57 ptr((char *)malloc(cap))
58 {
59 memcpy(ptr, s, cap);
60 }
cbuffer(const cbuffer & c)61 cbuffer(const cbuffer& c) :
62 cap(c.cap), ins(c.ins), ptr(nullptr) {
63 if (c.ptr) {
64 ptr = (char *)malloc(cap);
65 memcpy(ptr, c.ptr, cap);
66 }
67 }
operator =(const char * c)68 cbuffer& operator=(const char *c) {
69 if (c != ptr) {
70 if (c) {
71 ins = strlen(c);
72 cap = 1 + ins;
73 ptr = (char *)realloc(ptr, cap);
74 memcpy(ptr, c, cap);
75 } else if (ptr) {
76 cap = ins = 0;
77 free(ptr); ptr = nullptr;
78 }
79 }
80 return *this;
81 }
operator =(const cbuffer & c)82 cbuffer& operator=(const cbuffer& c) {
83 if (&c != this) {
84 if (c.ptr) {
85 ins = c.ins;
86 cap = 1 + ins;
87 ptr = (char *)realloc(ptr, cap);
88 memcpy(ptr, c.ptr, cap);
89 } else if (ptr) {
90 cap = ins = 0;
91 free(ptr); ptr = nullptr;
92 }
93 }
94 return *this;
95 }
operator +=(const char * c)96 cbuffer& operator+=(const char *c) {
97 if (c) {
98 size_t siz = 1 + strlen(c);
99 if (cap < ins + siz) {
100 cap = ins + siz;
101 ptr = (char *)realloc(ptr, cap);
102 }
103 memcpy(ptr + ins, c, siz);
104 ins += siz - 1;
105 }
106 return *this;
107 }
len() const108 int len() const {
109 return (int) ins;
110 }
push(int c)111 void push(int c) {
112 if (cap < 2 + ins) {
113 cap = 8 + 3 * cap / 2;
114 ptr = (char *)realloc(ptr, cap);
115 }
116 ptr[ins+1] = 0;
117 ptr[ins++] = (char) c;
118 }
pop()119 int pop() {
120 int result = 0;
121 if (ins > 0) {
122 result = ptr[--ins];
123 ptr[ins] = 0;
124 }
125 return result;
126 }
peek()127 char *peek() {
128 return ptr;
129 }
release()130 char *release() {
131 char *result = ptr;
132 cap = ins = 0;
133 ptr = nullptr;
134 return result;
135 }
~cbuffer()136 ~cbuffer() {
137 if (ptr) free(ptr);
138 }
operator const char*() const139 operator const char *() const {
140 return ptr;
141 }
last() const142 int last() const {
143 return ins > 0 ? ptr[ins-1] : 0;
144 }
isEmpty() const145 bool isEmpty() const {
146 return ins == 0;
147 }
nonempty() const148 bool nonempty() const {
149 return ins > 0;
150 }
151 };
152
153 class lowbuffer : public cbuffer {
154 public:
push(int c)155 void push(int c) {
156 cbuffer::push(ASCII::toLower(c));
157 }
operator +=(const char * c)158 lowbuffer& operator+=(const char *c) {
159 if (c) {
160 while (*c) push(*c++);
161 }
162 return *this;
163 }
164 };
165
166 // singly linked list of nodes which have a 'next' pointer.
167 template <class T>
168 class flist {
169 flist(const flist&);
170 flist& operator=(const flist&);
171 T *head, *tail;
172 public:
flist()173 flist() : head(nullptr), tail(nullptr) {}
flist(T * t)174 flist(T *t) : head(t), tail(t) { t->next = nullptr; }
add(T * t)175 void add(T *t) {
176 t->next = nullptr;
177 if (head) {
178 tail = tail->next = t;
179 } else {
180 tail = head = t;
181 }
182 }
first() const183 T *first() const { return head; }
last() const184 T *last() const { return tail; }
destroy()185 void destroy() {
186 for (; head; head = tail) {
187 tail = head->next;
188 delete head;
189 }
190 }
nonempty() const191 bool nonempty() const { return head != 0; }
operator T*() const192 operator T *() const { return head; }
193 };
194 // like flist, but delete all nodes when done.
195 template <class T>
196 class nlist : public flist<T> {
197 typedef flist<T> super;
198 public:
nlist()199 nlist() : super() {}
nlist(T * t)200 nlist(T *t) : super(t) {}
~nlist()201 ~nlist() { super::destroy(); }
202 };
203
204 class text_node {
205 public:
text_node(const char * t,int l,int f,int _x,int _y,int _w,int _h)206 text_node(const char *t, int l, int f, int _x, int _y, int _w, int _h) :
207 text(t), len(l), fl(f), x(_x), y(_y), tw(_w), th(_h), next(nullptr)
208 {
209 }
210
211 const char *text;
212 int len, fl;
213 int x, y;
214 int tw, th;
215 text_node *next;
216 };
217
218 class attr {
219 public:
220 enum attr_type {
221 noattr = 0,
222 href = 1,
223 id = 2,
224 name = 4,
225 rel = 8,
226 maxattr = rel
227 } atype;
228 mstring value;
229 attr *next;
attr()230 attr() : atype(noattr), value(null), next(nullptr) {}
attr(attr_type t,const mstring & s)231 attr(attr_type t, const mstring& s) : atype(t), value(s), next(nullptr) {}
attr(const attr & a)232 attr(const attr& a) : atype(a.atype), value(a.value), next(nullptr) {}
operator =(const attr & a)233 attr& operator=(const attr& a) {
234 if (&a != this) {
235 atype = a.atype;
236 value = a.value;
237 }
238 return *this;
239 }
operator bool() const240 operator bool() const { return atype > noattr; }
get_type(const char * s)241 static attr_type get_type(const char *s) {
242 if (0 == strcmp(s, "href")) return href;
243 if (0 == strcmp(s, "id")) return id;
244 if (0 == strcmp(s, "name")) return name;
245 if (0 == strcmp(s, "rel")) return rel;
246 return noattr;
247 }
248 };
249
250 class attr_list {
251 private:
252 attr_list(const attr_list&);
253 attr_list& operator=(const attr_list&);
254 nlist<attr> list;
255 public:
attr_list()256 attr_list() : list() {}
add(attr::attr_type typ,const mstring & s)257 void add(attr::attr_type typ, const mstring& s) {
258 if (typ > 0) {
259 list.add(new attr(typ, s));
260 }
261 }
get(int mask,attr * found) const262 bool get(int mask, attr *found) const {
263 for (attr *a = list.first(); a; a = a->next) {
264 if ((mask & a->atype) != 0) {
265 *found = *a;
266 return true;
267 }
268 }
269 return false;
270 }
get(int mask,const char * value,attr * found) const271 bool get(int mask, const char *value, attr *found) const {
272 for (attr *a = list.first(); a; a = a->next) {
273 if ((mask & a->atype) != 0 &&
274 0 == strcmp(value, a->value))
275 {
276 *found = *a;
277 return true;
278 }
279 }
280 return false;
281 }
282 };
283
284 class node {
285 public:
286 enum node_type {
287 unknown,
288 html, head, title,
289 body,
290 text, pre, line, paragraph, hrule,
291 h1, h2, h3, h4, h5, h6,
292 table, tr, td,
293 div,
294 ul, ol, li,
295 bold, font, italic,
296 center,
297 script,
298 style,
299 anchor, img,
300 tt, dl, dd, dt,
301 thead, tbody, tfoot,
302 link, code, meta, form, input,
303 section, figure, aside, footer, main,
304 };
305
node(node_type t)306 node(node_type t) :
307 type(t),
308 next(nullptr),
309 container(nullptr),
310 txt(nullptr),
311 wrap(),
312 xr(0),
313 yr(0),
314 attrs()
315 {
316 }
317 ~node();
318
319 node_type type;
320 node *next;
321 node *container;
322 const char *txt;
323 typedef nlist<text_node> text_list;
324 text_list wrap;
325 int xr, yr;
326 attr_list attrs;
327
add_attribute(attr::attr_type name,const char * value)328 void add_attribute(attr::attr_type name, const char *value) {
329 if (name > attr::noattr) {
330 attrs.add(name, value);
331 }
332 }
add_attribute(const char * name,const char * value)333 void add_attribute(const char *name, const char *value) {
334 add_attribute(attr::get_type(name), value);
335 }
get_attribute(int mask,attr * found) const336 bool get_attribute(int mask, attr *found) const {
337 return attrs.get(mask, found);
338 }
get_attribute(const char * name,attr * found) const339 bool get_attribute(const char *name, attr *found) const {
340 return attrs.get(attr::get_type(name), found);
341 }
342 node* find_attr(int mask, const char *value);
343
344 static const char *to_string(node_type type);
345 static node_type get_type(const char *buf);
346 };
347
~node()348 node::~node() {
349 delete next;
350 delete container;
351 free((void *)txt);
352 }
353
find_attr(int mask,const char * value)354 node* node::find_attr(int mask, const char *value) {
355 for (node *n = this; n; n = n->next) {
356 attr got;
357 if (n->attrs.get(mask, value, &got)) {
358 return n;
359 }
360 if (n->container) {
361 node *found = n->container->find_attr(mask, value);
362 if (found) return found;
363 }
364 }
365 return nullptr;
366 }
367
368
369 #define PRE 0x01
370 #define PRE1 0x02
371 #define BOLD 0x04
372 #define LINK 0x08
373 #define BIG 0x10
374 #define ITAL 0x20
375 #define MONO 0x40
376
377 #define sfPar 0x01
378 #define sfText 0x02
379
380 static void dump_tree(int level, node *n);
381
382 #define TS(x) case x : return #x
383
to_string(node_type type)384 const char *node::to_string(node_type type) {
385 switch (type) {
386 TS(unknown);
387 TS(html);
388 TS(head);
389 TS(title);
390 TS(body);
391 TS(text);
392 TS(pre);
393 TS(line);
394 TS(paragraph);
395 TS(hrule);
396 TS(anchor);
397 TS(h1);
398 TS(h2);
399 TS(h3);
400 TS(h4);
401 TS(h5);
402 TS(h6);
403 TS(table);
404 TS(tr);
405 TS(td);
406 TS(div);
407 TS(ul);
408 TS(ol);
409 TS(li);
410 TS(bold);
411 TS(font);
412 TS(italic);
413 TS(center);
414 TS(script);
415 TS(style);
416 TS(tt);
417 TS(dl);
418 TS(dd);
419 TS(dt);
420 TS(link);
421 TS(code);
422 TS(meta);
423 TS(thead);
424 TS(tbody);
425 TS(tfoot);
426 TS(img);
427 TS(form);
428 TS(input);
429 TS(section);
430 TS(figure);
431 TS(aside);
432 TS(footer);
433 TS(main);
434 }
435 if (complain) {
436 tlog("Unknown node_type %d, after %s, before %s", type,
437 type > unknown ? to_string((node_type)(type - 1)) : "",
438 type < input ? to_string((node_type)(type + 1)) : "");
439 }
440
441 return "??";
442 }
443
444 #undef TS
445 #define TS(x,y) if (0 == strcmp(buf, #x)) return node::y
446
get_type(const char * buf)447 node::node_type node::get_type(const char *buf)
448 {
449 TS(html, html);
450 TS(head, head);
451 TS(title, title);
452 TS(body, body);
453 TS(text, text);
454 TS(pre, pre);
455 TS(address, pre);
456 TS(br, line);
457 TS(p, paragraph);
458 TS(header, paragraph);
459 TS(article, paragraph);
460 TS(nav, paragraph);
461 TS(hr, hrule);
462 TS(a, anchor);
463 TS(h1, h1);
464 TS(h2, h2);
465 TS(h3, h3);
466 TS(h4, h4);
467 TS(h5, h5);
468 TS(h6, h6);
469 TS(table, table);
470 TS(tr, tr);
471 TS(td, td);
472 TS(div, div);
473 TS(span, div);
474 TS(ul, ul);
475 TS(ol, ol);
476 TS(li, li);
477 TS(b, bold);
478 TS(strong, bold);
479 TS(font, font);
480 TS(i, italic);
481 TS(em, italic);
482 TS(small, italic);
483 TS(center, center);
484 TS(script, script);
485 TS(svg, script);
486 TS(button, script);
487 TS(path, script);
488 TS(style, style);
489 TS(tt, tt);
490 TS(dl, dl);
491 TS(dd, dd);
492 TS(dt, dt);
493 TS(link, link);
494 TS(code, code);
495 TS(samp, code);
496 TS(kbd, code);
497 TS(var, code);
498 TS(option, code);
499 TS(label, code);
500 TS(blockquote, code);
501 TS(meta, meta);
502 TS(thead, thead);
503 TS(tbody, tbody);
504 TS(tfoot, tfoot);
505 TS(img, img);
506 TS(form, form);
507 TS(input, input);
508 TS(section, section);
509 TS(figure, figure);
510 TS(aside, aside);
511 TS(footer, footer);
512 TS(main, main);
513 if (*buf && buf[strlen(buf)-1] == '/') {
514 cbuffer cbuf(buf);
515 cbuf.pop();
516 return get_type(cbuf);
517 }
518 static const char* ignored[] = {
519 "abbr",
520 "g",
521 "g-emoji",
522 "include-fragment",
523 "rect",
524 "relative-time",
525 "th",
526 "time",
527 "time-ago",
528 };
529 for (unsigned i = 0; i < ACOUNT(ignored); ++i) {
530 if (0 == strcmp(buf, ignored[i]))
531 return node::unknown;
532 }
533 if (complain) {
534 tlog("unknown tag %s", buf);
535 }
536 return node::unknown;
537 }
538
ignore_comments(FILE * fp)539 static int ignore_comments(FILE *fp) {
540 int c = getc(fp);
541 if (c == '-') {
542 c = getc(fp);
543 if (c == '-') {
544 // comment
545 int n = 0;
546 while ((c = getc(fp)) != EOF) {
547 if (c == '>' && n >= 2) {
548 break;
549 }
550 n = (c == '-') ? (n + 1) : 0;
551 }
552 }
553 }
554 else if (c == '[' && (c = getc(fp)) == 'C') {
555 /* skip over <![CDATA[...]]> */
556 int n = 0;
557 while ((c = getc(fp)) != EOF) {
558 if (c == '>' && n >= 2) {
559 break;
560 }
561 n = (c == ']') ? (n + 1) : 0;
562 }
563 }
564 while (c != EOF && c != '>') {
565 c = getc(fp);
566 }
567 return c;
568 }
569
non_space(int c,FILE * fp)570 static int non_space(int c, FILE *fp) {
571 while (c != EOF && ASCII::isWhiteSpace(c)) {
572 c = getc(fp);
573 }
574 return c;
575 }
576
getc_non_space(FILE * fp)577 static int getc_non_space(FILE *fp) {
578 return non_space(getc(fp), fp);
579 }
580
parse(FILE * fp,int flags,node * parent,node * & nextsub,node::node_type & close)581 static node *parse(FILE *fp, int flags, node *parent, node *&nextsub, node::node_type &close) {
582 flist<node> nodes;
583
584 for (int c = getc(fp); c != EOF; c = (c == '<') ? c : getc(fp)) {
585 if (c == '<') {
586 c = getc(fp);
587 if (c == '!') {
588 c = ignore_comments(fp);
589 } else if (c == '/') {
590 // ignore </BR> </P> </LI> ...
591 lowbuffer buf;
592
593 c = getc_non_space(fp);
594 while (c != EOF && !SPACE(c) && c != '>') {
595 buf.push(c);
596 c = getc(fp);
597 }
598
599 node::node_type type = node::get_type(buf);
600 if (type == node::paragraph ||
601 type == node::line ||
602 type == node::hrule ||
603 type == node::link ||
604 type == node::unknown ||
605 (type == node::form && (!parent || parent->type != type)) ||
606 type == node::meta)
607 {
608 }
609 else if (parent) {
610 close = type;
611 break;
612 }
613 } else {
614 lowbuffer buf;
615
616 c = non_space(c, fp);
617 while (c != EOF && !SPACE(c) && c != '>') {
618 buf.push(c);
619 c = getc(fp);
620 }
621
622 node::node_type type = buf.nonempty()
623 ? node::get_type(buf) : node::unknown;
624 node *n = new node(type);
625 if (n == nullptr)
626 break;
627 while ((c = non_space(c, fp)) != '>' && c != EOF) {
628 lowbuffer abuf;
629
630 while (c != EOF && !SPACE(c) && c != '=' && c != '>') {
631 abuf.push(c);
632 c = getc(fp);
633 }
634 c = non_space(c, fp);
635 if (c == '=') {
636 cbuffer vbuf;
637 c = getc_non_space(fp);
638 if (c == '"') {
639 while ((c = getc(fp)) != EOF && c != '"') {
640 vbuf.push(c);
641 }
642 } else {
643 while (c != EOF && !SPACE(c) && c != '>') {
644 vbuf.push(c);
645 c = getc(fp);
646 }
647 }
648 if (abuf.nonempty())
649 n->add_attribute(abuf, vbuf);
650 }
651 }
652
653 if (type == node::line ||
654 type == node::hrule ||
655 type == node::paragraph||
656 type == node::link ||
657 type == node::meta)
658 {
659 nodes.add(n);
660 } else {
661 node *container = nullptr;
662
663 if (type == node::li ||
664 type == node::dt ||
665 type == node::dd ||
666 type == node::paragraph ||
667 type == node::line
668 )
669 {
670 if (parent &&
671 (parent->type == type ||
672 (type == node::dd && parent->type == node::dt) ||
673 (type == node::dt && parent->type == node::dd))
674 )
675 {
676 nextsub = n;
677 break;
678 }
679 }
680
681 node *nextsub;
682 node::node_type close_type;
683 do {
684 nextsub = nullptr;
685 close_type = node::unknown;
686 int fl = flags;
687 if (type == node::pre) fl |= PRE | PRE1;
688 container = parse(fp, fl, n, nextsub, close_type);
689 if (container)
690 n->container = container;
691 nodes.add(n);
692
693 if (nextsub) {
694 n = nextsub;
695 }
696 if (close_type != node::unknown) {
697 if (n->type != close_type)
698 return nodes;
699 }
700 } while (nextsub != nullptr);
701 }
702 }
703 }
704 else if (c != '<') {
705 cbuffer buf;
706
707 do {
708 if (c == '&') {
709 lowbuffer entity;
710
711 do {
712 entity.push(c);
713 c = getc(fp);
714 } while (ASCII::isAlnum(c) || c == '#');
715 if (c != ';')
716 ungetc(c, fp);
717 if (strcmp(entity, "&") == 0)
718 c = '&';
719 else if (strcmp(entity, "<") == 0)
720 c = '<';
721 else if (strcmp(entity, ">") == 0)
722 c = '>';
723 else if (strcmp(entity, """) == 0)
724 c = '"';
725 else if (strcmp(entity, " ") == 0)
726 c = ' '; // 32+128;
727 else if (strcmp(entity, " ") == 0)
728 c = ' '; // 32+128;
729 else if (strcmp(entity, "​") == 0)
730 c = ' '; // 32+128; zero width space
731 else if (strcmp(entity, "–") == 0
732 || strcmp(entity, "&ndash") == 0)
733 c = '-'; // en dash
734 else if (strcmp(entity, "—") == 0
735 || strcmp(entity, "&mdash") == 0)
736 c = '-'; // em dash
737 else if (strcmp(entity, "’") == 0)
738 c = '\''; // right single quote
739 else if (strcmp(entity, "“") == 0) {
740 buf += "``"; // left double quotes
741 continue;
742 }
743 else if (strcmp(entity, "”") == 0) {
744 buf += "''"; // right double quotes
745 continue;
746 }
747 else if (strcmp(entity, "•") == 0
748 || strcmp(entity, "·") == 0)
749 c = '*'; // bullet
750 else if (strcmp(entity, "…") == 0
751 || strcmp(entity, "&hellip") == 0) {
752 buf += "..."; // horizontal ellipsis
753 continue;
754 }
755 else if (strcmp(entity, "→") == 0) {
756 buf += "->"; // rightwards arrow
757 continue;
758 }
759 else if (strcmp(entity, "©") == 0) {
760 buf += "(c)"; // copyright symbol
761 continue;
762 }
763 else if (strcmp(entity, "®") == 0) {
764 buf += "(R)"; // registered sign
765 continue;
766 }
767 else if (entity.len() > 2 && !strcmp(entity + 2, "tilde")) {
768 c = entity[1];
769 }
770 else if (entity.len() > 2 && !strcmp(entity + 2, "acute")) {
771 c = entity[1];
772 }
773 else if (entity.len() > 2 && !strcmp(entity + 2, "uml")) {
774 c = entity[1];
775 }
776 else {
777 unsigned special = 0;
778 int len = 0;
779 if (1 == sscanf(entity, "&#%u%n", &special, &len)
780 && len == entity.len()
781 && inrange(special, 32U, 126U))
782 {
783 c = (char) special;
784 }
785 else {
786 if (complain) {
787 tlog("unknown special '%s'", entity.peek());
788 }
789 c = ' ';
790 }
791 }
792 }
793 if (c == '\r') {
794 c = getc(fp);
795 if (c != '\n')
796 ungetc(c, fp);
797 c = '\n';
798 }
799 if (!(flags & PRE)) {
800 if (SPACE(c))
801 c = ' ';
802 }
803 if ((flags & PRE1) && c == '\n')
804 ;
805 else if (c != ' ' || (flags & PRE) ||
806 buf.isEmpty() || buf.last() != ' ')
807 {
808 buf.push(c);
809 }
810 flags &= ~PRE1;
811 } while ((c = getc(fp)) != EOF && c != '<');
812
813 if (c == '<') {
814 int k = getc(fp);
815 if (k == '/') {
816 if (SPACE(buf.last()))
817 buf.pop();
818 }
819 ungetc(k, fp);
820 }
821
822 if (buf.nonempty()) {
823 node *n = new node(node::text);
824 n->txt = buf.release();
825 nodes.add(n);
826 }
827 }
828 }
829 return nodes;
830 }
831
832 class History {
833 private:
834 MStringArray array;
835 int where;
836 public:
History()837 History() : where(-1) { }
empty() const838 bool empty() const { return array.isEmpty(); }
size() const839 int size() const { return array.getCount(); }
get(int i) const840 const char* get(int i) const { return array[i]; }
push(const mstring & s)841 void push(const mstring& s) {
842 if (where == -1 || (s.nonempty() && s != get(where))) {
843 for (int k = size() - 1; k > where; --k) {
844 array.remove(k);
845 }
846 array.insert(++where, s);
847 }
848 }
current()849 mstring& current() {
850 if (empty()) array.insert(++where, null);
851 return array[where];
852 }
hasLeft() const853 bool hasLeft() const { return where > 0; }
left()854 bool left() {
855 return hasLeft() ? (--where, true) : false;
856 }
hasRight() const857 bool hasRight() const { return where + 1 < size(); }
right()858 bool right() {
859 return hasRight() ? (++where, true) : false;
860 }
hasFirst() const861 bool hasFirst() const { return 0 < size(); }
first()862 bool first() {
863 return hasFirst() ? (where = 0, true) : false;
864 }
865 };
866
867 class FontEntry {
868 public:
869 int size;
870 int flag;
871 const char *core;
872 const char *xeft;
873 YFont font;
874 };
875 class FontTable {
876 public:
877 static FontEntry table[];
878 static YFont get(int size, int flags);
reset()879 static void reset() {
880 for (int k = 0; table[k].size; ++k) {
881 table[k].font = null;
882 }
883 }
884 };
885 FontEntry FontTable::table[] = {
886 { 12, 0,
887 "-adobe-helvetica-medium-r-normal--12-120-75-75-p-67-iso8859-1",
888 "Snap:size=10,sans-serif:size=10" },
889 { 14, 0,
890 "-adobe-helvetica-medium-r-normal--14-140-75-75-p-77-iso8859-1",
891 "Snap:size=12,sans-serif:size=12" },
892 { 18, 0,
893 "-adobe-helvetica-medium-r-normal--18-180-75-75-p-98-iso8859-1",
894 "Snap:size=14,sans-serif:size=14" },
895 { 12, BOLD,
896 "-adobe-helvetica-bold-r-normal--12-120-75-75-p-70-iso8859-1",
897 "Snap:size=10:bold,sans-serif:size=10:bold" },
898 { 14, BOLD,
899 "-adobe-helvetica-bold-r-normal--14-140-75-75-p-82-iso8859-1",
900 "Snap:size=12:bold,sans-serif:size=12:bold" },
901 { 18, BOLD,
902 "-adobe-helvetica-bold-r-normal--18-180-75-75-p-103-iso8859-1",
903 "Snap:size=14:bold,sans-serif:size=14:bold" },
904 { 12, MONO,
905 "-adobe-courier-medium-r-normal--12-120-75-75-m-70-iso8859-1",
906 "courier:size=10,monospace:size=10" },
907 { 17, MONO | BOLD,
908 "-adobe-courier-bold-r-normal--17-120-100-100-m-100-iso8859-1",
909 "courier:size=14,monospace:size=14" },
910 { 12, ITAL,
911 "-adobe-helvetica-medium-o-normal--12-120-75-75-p-67-iso8859-1",
912 "sans-serif:size=10:oblique" },
913 { 14, ITAL,
914 "-adobe-helvetica-medium-o-normal--14-140-75-75-p-78-iso8859-1",
915 "sans-serif:size=12:oblique" },
916 { 0, 0, nullptr, nullptr },
917 };
get(int size,int flags)918 YFont FontTable::get(int size, int flags) {
919 int best = 0;
920 int diff = abs(size - table[best].size);
921 for (int i = 1; table[i].size > 0; ++i) {
922 int delt = abs(size - table[i].size);
923 if (delt < diff) {
924 best = i;
925 diff = delt;
926 }
927 else if (diff == delt) {
928 int bflag = table[best].flag;
929 int iflag = table[i].flag;
930 if ((bflag & BOLD) != (iflag & BOLD)) {
931 if ((iflag & BOLD) == (flags & BOLD)) {
932 best = i;
933 }
934 }
935 else if ((bflag & MONO) != (iflag & MONO)) {
936 if ((iflag & MONO) == (flags & MONO)) {
937 best = i;
938 }
939 }
940 else if ((bflag & ITAL) != (iflag & ITAL)) {
941 if ((iflag & ITAL) == (flags & ITAL)) {
942 best = i;
943 }
944 }
945 }
946 }
947 if (table[best].font == null) {
948 const char* xeft = noFreeType ? nullptr : table[best].xeft;
949 YFontName fontName(&table[best].core, &xeft);
950 table[best].font = fontName;
951 }
952 return table[best].font;
953 }
954
955 class HTListener {
956 public:
957 virtual void activateURL(mstring url, bool relative = false) = 0;
958 virtual void openBrowser(mstring url) = 0;
959 virtual void handleClose() = 0;
960 protected:
~HTListener()961 virtual ~HTListener() {}
962 };
963
964 class ActionItem : public YAction {
965 private:
966 YMenuItem *item;
967 ActionItem(const ActionItem&);
968 ActionItem& operator=(const ActionItem&);
969 public:
ActionItem()970 ActionItem() : item(nullptr) {}
~ActionItem()971 ~ActionItem() { }
operator =(YMenuItem * menuItem)972 void operator=(YMenuItem* menuItem) { item = menuItem; }
operator ->()973 YMenuItem* operator->() { return item; }
974 };
975
976 class HTextView: public YWindow,
977 public YScrollBarListener,
978 public YScrollable,
979 public YActionListener,
980 public YInputListener
981 {
982 public:
983 HTextView(HTListener *fL, YScrollView *v, YWindow *parent);
984 ~HTextView();
985
986 void find_link(node *n);
987
setData(node * root)988 void setData(node *root) {
989 delete fRoot;
990 fRoot = root;
991 tx = ty = 0;
992 layout();
993
994 actionPrev->setEnabled(false);
995 actionNext->setEnabled(false);
996 actionContents->setEnabled(false);
997
998 nextURL = null;
999 prevURL = null;
1000 contentsURL = null;
1001
1002 find_link(fRoot);
1003 if (contentsURL == null) {
1004 node *n = fRoot->find_attr(attr::id | attr::name, "toc");
1005 if (n) {
1006 contentsURL = "#toc";
1007 actionContents->setEnabled(true);
1008 }
1009 }
1010
1011 actionIndex->setEnabled(history.hasFirst());
1012 actionLeft->setEnabled(history.hasLeft());
1013 actionRight->setEnabled(history.hasRight());
1014 }
1015
1016
1017 void par(int &state, int &x, int &y, unsigned &h, const int left);
1018 void epar(int &state, int &x, int &y, unsigned &h, const int left);
1019 void layout();
1020 void layout(node *parent, node *n1, int left, int right, int &x, int &y, unsigned &w, unsigned &h, int flags, int &state);
1021 void draw(Graphics &g, node *n1, bool isHref = false);
1022 node *find_node(node *n, int x, int y, node *&anchor, node::node_type type);
1023 void find_fragment(const char *frag);
1024 bool findNext(node* n1);
1025 void startSearch();
1026
paint(Graphics & g,const YRect & r)1027 virtual void paint(Graphics &g, const YRect &r) {
1028 g.setColor(bg);
1029 g.fillRect(r.x(), r.y(), r.width(), r.height());
1030
1031 if (ty < 20) {
1032 int sz = 19;
1033 g.setColor(reverseVideo ? normalFg.darker().reverse() : bg.darker());
1034 g.fillRect(width() - 20, 20 - ty - sz, sz, sz, 1);
1035 g.setColor(bg.brighter());
1036 for (int i = 0; i <= 2; ++i) {
1037 g.drawLine(width() - 20 + 2, 20 - ty - 15 + i * 5,
1038 width() - 20 + 16, 20 - ty - 15 + i * 5);
1039 }
1040 }
1041
1042 g.setColor(normalFg);
1043 g.setFont(font);
1044
1045 draw(g, fRoot);
1046 }
handleExpose(const XExposeEvent & expose)1047 virtual void handleExpose(const XExposeEvent& expose) {
1048 }
1049
isFocusTraversable()1050 bool isFocusTraversable() {
1051 return true;
1052 }
1053
repaint()1054 virtual void repaint() {
1055 graphicsBuffer.paint();
1056 }
1057
configure(const YRect2 & r)1058 virtual void configure(const YRect2& r) {
1059 if (r.resized()) {
1060 resetScroll();
1061 repaint();
1062 }
1063 }
1064
resetScroll()1065 void resetScroll() {
1066 fVerticalScroll->setValues(ty, height(), 0, contentHeight());
1067 fVerticalScroll->setBlockIncrement(max(40U, height()) / 2);
1068 fVerticalScroll->setUnitIncrement(font->height());
1069 fHorizontalScroll->setValues(tx, width(), 0, contentWidth());
1070 fHorizontalScroll->setBlockIncrement(max(40U, width()) / 2);
1071 fHorizontalScroll->setUnitIncrement(20);
1072 if (fScrollView)
1073 fScrollView->layout();
1074 }
1075
setPos(int x,int y)1076 void setPos(int x, int y) {
1077 if (x != tx || y != ty) {
1078 int dx = x - tx;
1079 int dy = y - ty;
1080
1081 tx = x;
1082 ty = y;
1083
1084 graphicsBuffer.scroll(dx, dy);
1085 }
1086 }
1087
scroll(YScrollBar * sb,int delta)1088 virtual void scroll(YScrollBar *sb, int delta) {
1089 if (sb == fHorizontalScroll)
1090 setPos(tx + delta, ty);
1091 else if (sb == fVerticalScroll)
1092 setPos(tx, ty + delta);
1093 }
move(YScrollBar * sb,int pos)1094 virtual void move(YScrollBar *sb, int pos) {
1095 if (sb == fHorizontalScroll)
1096 setPos(pos, ty);
1097 else if (sb == fVerticalScroll)
1098 setPos(tx, pos);
1099 }
1100
contentWidth()1101 unsigned contentWidth() {
1102 return conWidth;
1103 }
contentHeight()1104 unsigned contentHeight() {
1105 return conHeight;
1106 }
1107
1108 virtual void handleClick(const XButtonEvent &up, int /*count*/);
1109
actionPerformed(YAction action,unsigned int modifiers=0)1110 virtual void actionPerformed(YAction action, unsigned int modifiers = 0) {
1111 if (action == actionClose) {
1112 listener->handleClose();
1113 }
1114 else if (action == actionBrowser) {
1115 listener->openBrowser(history.current());
1116 }
1117 else if (action == actionReload) {
1118 listener->activateURL(history.current(), false);
1119 }
1120 else if (action == actionNext) {
1121 if (actionNext->isEnabled())
1122 listener->activateURL(nextURL, true);
1123 }
1124 else if (action == actionPrev) {
1125 if (actionPrev->isEnabled())
1126 listener->activateURL(prevURL, true);
1127 }
1128 else if (action == actionContents) {
1129 if (actionContents->isEnabled())
1130 listener->activateURL(contentsURL, true);
1131 }
1132 else if (action == actionIndex) {
1133 if (actionIndex->isEnabled() && history.first())
1134 listener->activateURL(history.current());
1135 }
1136 else if (action == actionLeft) {
1137 if (actionLeft->isEnabled() && history.left())
1138 listener->activateURL(history.current());
1139 }
1140 else if (action == actionRight) {
1141 if (actionRight->isEnabled() && history.right())
1142 listener->activateURL(history.current());
1143 }
1144 else if (action == actionLink[0]) {
1145 if (actionLink[0]->isEnabled())
1146 listener->activateURL(ICEWM_1);
1147 }
1148 else if (action == actionLink[1]) {
1149 if (actionLink[1]->isEnabled())
1150 listener->activateURL(ICEWMBG_1);
1151 }
1152 else if (action == actionLink[2]) {
1153 if (actionLink[2]->isEnabled())
1154 listener->activateURL(ICESOUND_1);
1155 }
1156 else if (action == actionLink[3]) {
1157 if (actionLink[3]->isEnabled())
1158 listener->activateURL(ICEWM_FAQ);
1159 }
1160 else if (action == actionLink[4]) {
1161 if (actionLink[4]->isEnabled())
1162 listener->activateURL(ICEHELPIDX);
1163 }
1164 else if (action == actionLink[5]) {
1165 if (actionLink[5]->isEnabled())
1166 listener->activateURL(PACKAGE_BUGREPORT);
1167 }
1168 else if (action == actionLink[6]) {
1169 if (actionLink[6]->isEnabled())
1170 listener->activateURL(THEME_HOWTO);
1171 }
1172 else if (action == actionLink[7]) {
1173 if (actionLink[7]->isEnabled())
1174 listener->activateURL(ICEWM_SITE);
1175 }
1176 else if (action == actionLink[8]) {
1177 if (actionLink[8]->isEnabled())
1178 listener->activateURL(ICEGIT_SITE);
1179 }
1180 #ifdef CONFIG_XFREETYPE
1181 else if (action == actionFreeType) {
1182 noFreeType = !noFreeType;
1183 actionFreeType->setChecked(!noFreeType);
1184 FontTable::reset();
1185 actionPerformed(actionReload);
1186 }
1187 #endif
1188 else if (action == actionReversed) {
1189 reverseVideo = !reverseVideo;
1190 actionReversed->setChecked(reverseVideo);
1191 if (reverseVideo) {
1192 bg = FOREGROUND;
1193 normalFg = BACKGROUND;
1194 linkFg = LINK_COLOR;
1195 linkFg.reverse();
1196 } else {
1197 bg = BACKGROUND;
1198 normalFg = FOREGROUND;
1199 linkFg = LINK_COLOR;
1200 }
1201 repaint();
1202 YScrollBar::reverseVideo();
1203 if (fHorizontalScroll->visible())
1204 fHorizontalScroll->repaint();
1205 if (fVerticalScroll->visible())
1206 fVerticalScroll->repaint();
1207 }
1208 else if (action == actionFind) {
1209 if (input == nullptr) {
1210 input = new YInputLine(this, this);
1211 }
1212 if (input) {
1213 input->setBorderWidth(2);
1214 input->setGeometry(YRect(1, 1, 250, 25));
1215 input->show();
1216 xapp->sync();
1217 setFocus(input);
1218 input->requestFocus(true);
1219 input->gotFocus();
1220 }
1221 }
1222 else if (action == actionSearch) {
1223 startSearch();
1224 }
1225 }
1226
1227 bool handleKey(const XKeyEvent &key);
1228 void handleButton(const XButtonEvent &button);
1229 void addHistory(const mstring& path);
1230 private:
1231 node *fRoot;
1232
1233 int tx, ty;
1234 unsigned conWidth;
1235 unsigned conHeight;
1236
1237 YFont font;
1238 int fontFlag, fontSize;
1239 YColorName bg, normalFg, linkFg, hrFg;
1240
1241 YScrollView *fScrollView;
1242 YScrollBar *fVerticalScroll;
1243 YScrollBar *fHorizontalScroll;
1244
1245 YMenu *menu;
1246 ActionItem actionClose;
1247 ActionItem actionLeft;
1248 ActionItem actionRight;
1249 ActionItem actionIndex;
1250 ActionItem actionFind;
1251 ActionItem actionSearch;
1252 ActionItem actionPrev;
1253 ActionItem actionNext;
1254 ActionItem actionReload;
1255 ActionItem actionBrowser;
1256 ActionItem actionContents;
1257 ActionItem actionLink[10];
1258 ActionItem actionFreeType;
1259 ActionItem actionReversed;
1260 HTListener *listener;
1261
1262 mstring search;
1263 mstring prevURL;
1264 mstring nextURL;
1265 mstring contentsURL;
1266 History history;
1267 GraphicsBuffer graphicsBuffer;
1268
flagFont(int n)1269 void flagFont(int n) {
1270 int mask = (n & (MONO | BOLD | ITAL)) | ((n & PRE) ? MONO : 0);
1271 int size = (n & BIG) ? 18 : 14;
1272 if (mask != fontFlag || size != fontSize) {
1273 font = FontTable::get(size, mask);
1274 fontSize = size;
1275 fontFlag = mask;
1276 }
1277 }
1278 class Flags {
1279 public:
1280 HTextView *view;
1281 const int oldf, newf;
Flags(HTextView * v,int o,int n)1282 Flags(HTextView *v, int o, int n) : view(v), oldf(o), newf(n) {
1283 view->flagFont(newf);
1284 }
~Flags()1285 ~Flags() { view->flagFont(oldf); }
operator int() const1286 operator int() const { return newf; }
1287 };
1288
inputReturn(YInputLine * inputline)1289 void inputReturn(YInputLine* inputline) {
1290 search = input->getText();
1291 delete input; input = nullptr;
1292 startSearch();
1293 }
inputEscape(YInputLine * inputline)1294 void inputEscape(YInputLine* inputline) {
1295 delete input; input = nullptr;
1296 }
inputLostFocus(YInputLine * inputline)1297 void inputLostFocus(YInputLine* inputline) {
1298 delete input; input = nullptr;
1299 }
1300 YInputLine* input;
1301 };
1302
HTextView(HTListener * fL,YScrollView * v,YWindow * parent)1303 HTextView::HTextView(HTListener *fL, YScrollView *v, YWindow *parent):
1304 YWindow(parent), fRoot(nullptr),
1305 bg(BACKGROUND),
1306 normalFg(FOREGROUND),
1307 linkFg(LINK_COLOR),
1308 hrFg(RULE_COLOR),
1309 fScrollView(v),
1310 fVerticalScroll(v->getVerticalScrollBar()),
1311 fHorizontalScroll(v->getHorizontalScrollBar()),
1312 listener(fL),
1313 graphicsBuffer(this),
1314 input(nullptr)
1315 {
1316 fScrollView->setListener(this);
1317 tx = ty = 0;
1318 conWidth = conHeight = 0;
1319 fontFlag = 0;
1320 fontSize = 0;
1321 flagFont(0);
1322 setTitle("HTextView");
1323
1324 menu = new YMenu();
1325 menu->setActionListener(this);
1326 actionLeft = menu->addItem(_("Back"), 0, _("Alt+Left"), actionLeft);
1327 actionLeft->setEnabled(false);
1328 actionRight = menu->addItem(_("Forward"), 0, _("Alt+Right"), actionRight);
1329 actionRight->setEnabled(false);
1330 menu->addSeparator();
1331 actionPrev = menu->addItem(_("Previous"), 0, null, actionPrev);
1332 actionNext = menu->addItem(_("Next"), 0, null, actionNext);
1333 menu->addSeparator();
1334 actionContents = menu->addItem(_("Contents"), 0, null, actionContents);
1335 actionIndex = menu->addItem(_("Index"), 0, null, actionIndex);
1336 actionIndex->setEnabled(false);
1337 actionFind = menu->addItem(_("Find..."), 0, _("Ctrl+F"), actionFind);
1338 actionSearch = menu->addItem(_("Find next"), 0, _("F3"), actionSearch);
1339 menu->addSeparator();
1340 actionBrowser = menu->addItem(_("Open in Browser"), 0, _("Ctrl+B"),
1341 actionBrowser);
1342 actionReload = menu->addItem(_("Reload"), 0, _("Ctrl+R"), actionReload);
1343 actionClose = menu->addItem(_("Close"), 0, _("Ctrl+Q"), actionClose);
1344 menu->addSeparator();
1345
1346 int k = -1;
1347 k++;
1348 actionLink[k] = menu->addItem(_("Icewm(1)"), 2, _("Ctrl+I"), actionLink[k]);
1349 k++;
1350 actionLink[k] = menu->addItem(_("Icewmbg(1)"), 5, null, actionLink[k]);
1351 k++;
1352 actionLink[k] = menu->addItem(_("Icesound(1)"), 3, null, actionLink[k]);
1353 k++;
1354 actionLink[k] = menu->addItem(_("FAQ"), 0, null, actionLink[k]);
1355 k++;
1356 actionLink[k] = menu->addItem(_("Manual"), 0, _("Ctrl+M"), actionLink[k]);
1357 k++;
1358 actionLink[k] = menu->addItem(_("Support"), 0, null, actionLink[k]);
1359 k++;
1360 actionLink[k] = menu->addItem(_("Theme Howto"), 0, _("Ctrl+T"), actionLink[k]);
1361 k++;
1362 actionLink[k] = menu->addItem(_("Website"), 0, _("Ctrl+W"), actionLink[k]);
1363 k++;
1364 actionLink[k] = menu->addItem(_("Github"), 0, null, actionLink[k]);
1365
1366 menu->addSeparator();
1367 #ifdef CONFIG_XFREETYPE
1368 actionFreeType = menu->addItem("FreeType fonts", -1, _("Ctrl+X"), actionFreeType);
1369 actionFreeType->setChecked(!noFreeType);
1370 #else
1371 noFreeType = true;
1372 #endif
1373 actionReversed = menu->addItem("Reverse video", -1, _("Ctrl+V"), actionReversed);
1374 actionReversed->setChecked(reverseVideo);
1375 }
1376
~HTextView()1377 HTextView::~HTextView() {
1378 delete fRoot;
1379 delete input;
1380 delete menu;
1381 }
1382
addHistory(const mstring & path)1383 void HTextView::addHistory(const mstring& path) {
1384 history.push(path);
1385 actionLeft->setEnabled(history.hasLeft());
1386 actionRight->setEnabled(history.hasRight());
1387 actionIndex->setEnabled(history.hasFirst());
1388 }
1389
find_node(node * n,int x,int y,node * & anchor,node::node_type type)1390 node *HTextView::find_node(node *n, int x, int y, node *&anchor, node::node_type type) {
1391
1392 for (; n; n = n->next) {
1393 if (n->container) {
1394 node *f = find_node(n->container, x, y, anchor, type);
1395 if (f) {
1396 if (anchor == nullptr && n->type == type)
1397 anchor = n;
1398 return f;
1399 }
1400 }
1401 for (text_node *t = n->wrap; t; t = t->next) {
1402 if (x >= t->x && x < t->x + t->tw &&
1403 y < t->y && y > t->y - t->th)
1404 return n;
1405 }
1406 }
1407 return nullptr;
1408 }
1409
find_link(node * n)1410 void HTextView::find_link(node *n) {
1411 for (; n; n = n->next) {
1412 if (n->type == node::link) {
1413 attr rel, href;
1414 if (n->get_attribute("rel", &rel) &&
1415 n->get_attribute("href", &href))
1416 {
1417 if (strcasecmp(rel.value, "previous") == 0) {
1418 prevURL = href.value;
1419 actionPrev->setEnabled(true);
1420 }
1421 if (strcasecmp(rel.value, "next") == 0) {
1422 nextURL = href.value;
1423 actionNext->setEnabled(true);
1424 }
1425 if (strcasecmp(rel.value, "contents") == 0) {
1426 contentsURL = href.value;
1427 actionContents->setEnabled(true);
1428 }
1429 }
1430 }
1431 if (n->container)
1432 find_link(n->container);
1433 }
1434 }
1435
find_fragment(const char * frag)1436 void HTextView::find_fragment(const char *frag) {
1437 node *n = fRoot->find_attr(attr::id | attr::name, frag);
1438 if (n) {
1439 int y = max(0, min(n->yr, (int) contentHeight() - (int) height()));
1440 setPos(0, y);
1441 fVerticalScroll->setValue(y);
1442 fHorizontalScroll->setValue(0);
1443 }
1444 }
1445
layout()1446 void HTextView::layout() {
1447 int state = sfPar;
1448 int x = ViewerLeftMargin, y = ViewerTopMargin;
1449 int left = x, right = width() - x;
1450 conWidth = conHeight = 0;
1451 layout(nullptr, fRoot, left, right, x, y, conWidth, conHeight, 0, state);
1452 conHeight += font->height();
1453 resetScroll();
1454 }
1455
addState(int & state,int value)1456 void addState(int &state, int value) {
1457 state |= value;
1458 //msg("addState=%d %d", state, value);
1459 }
1460
removeState(int & state,int value)1461 void removeState(int &state, int value) {
1462 state &= ~value;
1463 //msg("removeState=%d %d", state, value);
1464 }
1465
par(int & state,int & x,int & y,unsigned & h,const int left)1466 void HTextView::par(int &state, int &x, int &y, unsigned &h, const int left) {
1467 if (!(state & sfPar)) {
1468 h += font->height();
1469 x = left;
1470 y = h;
1471 addState(state, sfPar);
1472 }
1473 }
1474
epar(int & state,int & x,int & y,unsigned & h,const int left)1475 void HTextView::epar(int &state, int &x, int &y, unsigned &h, const int left) {
1476 if ((x > left) || ((state & (sfText | sfPar)) == sfText)) {
1477 h += font->height();
1478 x = left;
1479 y = h;
1480 removeState(state, sfText);
1481 }
1482 removeState(state, sfPar);
1483 }
1484
layout(node * parent,node * n1,int left,int right,int & x,int & y,unsigned & w,unsigned & h,int flags,int & state)1485 void HTextView::layout(
1486 node *parent, node *n1, int left, int right,
1487 int &x, int &y, unsigned &w, unsigned &h,
1488 int flags, int &state)
1489 {
1490 ///puts("{");
1491 for (node *n = n1; n; n = n->next) {
1492 n->xr = x;
1493 n->yr = y;
1494 switch (n->type) {
1495 case node::head:
1496 case node::meta:
1497 case node::title:
1498 case node::script:
1499 case node::style:
1500 break;
1501 case node::h1:
1502 case node::h2:
1503 case node::h3:
1504 case node::h4:
1505 case node::h5:
1506 case node::h6:
1507 if (x > left) {
1508 x = left;
1509 y = h + font->height();
1510 }
1511 removeState(state, sfPar);
1512 x = left;
1513 n->xr = x;
1514 n->yr = y;
1515 if (n->container) {
1516 int h1bold = n->type == node::h1 ? BOLD : 0;
1517 Flags fl(this, flags, flags | BIG | h1bold);
1518 layout(n, n->container, n->xr, right, x, y, w, h, fl, state);
1519 }
1520 x = left;
1521 y = h + font->height();
1522 addState(state, sfText);
1523 removeState(state, sfPar);
1524 break;
1525 case node::text:
1526 n->wrap.destroy();
1527 for (const char *b = n->txt, *c; *b; b = c) {
1528 int wc = 0;
1529
1530 if (flags & PRE) {
1531 c = b;
1532 while (*c && *c != '\n')
1533 c++;
1534 wc = font->textWidth(b, c - b);
1535 } else {
1536 if (x == left)
1537 while (SPACE(*b))
1538 b++;
1539
1540 c = b;
1541
1542 do {
1543 const char *d = c;
1544
1545 if (SPACE(*d))
1546 while (SPACE(*d))
1547 d++;
1548 else
1549 while (*d && !SPACE(*d))
1550 d++;
1551
1552 int w1 = font->textWidth(b, d - b);
1553
1554 if (x + w1 < right) {
1555 wc = w1;
1556 c = d;
1557 } else if (x == left && wc == 0) {
1558 wc = w1;
1559 c = d;
1560 break;
1561 } else if (wc == 0) {
1562 x = left;
1563 y = h;
1564 while (SPACE(*c))
1565 c++;
1566 b = c;
1567 } else
1568 break;
1569 ///msg("width=%d %d / %d / %d", wc, x, left, right);
1570 } while (*c);
1571 }
1572
1573 if (!(flags & PRE) && x == left) {
1574 while (SPACE(*b)) b++;
1575 }
1576 if ((flags & PRE) || c - b > 0) {
1577 par(state, x, y, h, left);
1578 addState(state, sfText);
1579
1580 n->wrap.add(new text_node(b, c - b, flags,
1581 x, y + 12, wc, font->height()));
1582 if (y + (int)font->height() > (int)h)
1583 h = y + font->height();
1584
1585 x += wc;
1586 if (x > (int)w)
1587 w = x;
1588
1589 if ((flags & PRE)) {
1590 if (*c == '\n') {
1591 c++;
1592 x = left;
1593 y = h;
1594 }
1595 }
1596 }
1597 //msg("len=%d x=%d left=%d %s", c - b, x, left, b);
1598 }
1599 break;
1600 case node::line:
1601 //puts("<BR>");
1602 if (x != left) {
1603 y = h;
1604 //h += font->height();
1605 } else {
1606 h += font->height();
1607 y = h;
1608 }
1609 x = left;
1610 break;
1611 case node::paragraph:
1612 if (parent) {
1613 if (parent->type != node::dd && parent->type != node::li) {
1614 removeState(state, sfPar);
1615 }
1616 }
1617 //puts("<P>");
1618 //par(state, x, y, h, left);
1619 x = left;
1620 y = h;
1621 n->xr = x;
1622 n->yr = y;
1623 break;
1624 case node::hrule:
1625 //puts("<HR>");
1626 //epar(state, x, y, h, left);
1627 if ((state & sfText) && !(state & sfPar))
1628 h += font->height();
1629 x = left;
1630 n->xr = x;
1631 n->yr = h;
1632 h += 10;
1633 y = h;
1634 addState(state, sfPar);
1635 break;
1636 case node::center:
1637 x = left;
1638 y = h;
1639 if (n->container) {
1640 layout(n, n->container, n->xr, right, x, y, w, h, flags, state);
1641 // !!! center
1642 }
1643 h += font->height();
1644 x = left;
1645 y = h;
1646 break;
1647 case node::anchor:
1648 if (n->container) {
1649 layout(n, n->container, left, right, x, y, w, h, flags, state);
1650 }
1651 break;
1652 case node::ul:
1653 case node::ol:
1654 //puts("<UL>");
1655 //epar(state, x, y, h, left);
1656 if (!(parent && parent->type == node::li)) {
1657 removeState(state, sfPar);
1658 }
1659 y = h;
1660 x = left;
1661 n->xr = x;
1662 n->yr = y;
1663 x += 40;
1664 if (n->container) {
1665 layout(n, n->container, x, right, x, y, w, h, flags, state);
1666 }
1667 addState(state, sfText);
1668 x = left;
1669 y = h;
1670 //state &= ~sfPar;
1671 //par(state, x, y, h, left);
1672 //x = left;
1673 //y = h + font->height();
1674 removeState(state, sfPar);
1675 addState(state, sfText);
1676 break;
1677 case node::dl:
1678 //puts("<UL>");
1679 epar(state, x, y, h, left);
1680 n->xr = x;
1681 n->yr = y;
1682 if (parent && parent->type == node::dt)
1683 x += 40;
1684 if (n->container) {
1685 //puts("<DL>\n");
1686 layout(n, n->container, x, right, x, y, w, h, flags, state);
1687 //puts("</DL>\n");
1688 }
1689 addState(state, sfText);
1690 x = left;
1691 y = h;
1692 removeState(state, sfPar);
1693 addState(state, sfText);
1694 //state &= ~sfPar;
1695 //par(state, x, y, h, left);
1696 //x = left;
1697 //y = h + font->height();
1698 break;
1699 case node::dt:
1700 //puts("<LI>");
1701 //if (state & sfText)
1702 y = h;
1703 x = left;
1704 n->xr = x;
1705 n->yr = y;
1706
1707 addState(state, sfPar);
1708 if (n->container) {
1709 layout(n, n->container, left, right, x, y, w, h, flags, state);
1710 }
1711 y = h;
1712 x = left;
1713 //removeState(state, sfPar | sfText);
1714 break;
1715 case node::dd:
1716 //puts("<DT>");
1717 //if (state & sfText)
1718 epar(state, x, y, h, left);
1719 y = h;
1720 x = left + 40;
1721 n->xr = x;
1722 n->yr = y;
1723
1724 addState(state, sfPar);
1725 if (n->container) {
1726 //puts("<DD>\n");
1727 layout(n, n->container, x, right, x, y, w, h, flags, state);
1728 //puts("</DD>\n");
1729 h += font->height();
1730 }
1731 y = h;
1732 x = left;
1733 //removeState(state, sfPar | sfText);
1734 break;
1735
1736 case node::li:
1737 //puts("<LI>");
1738
1739 //msg("state=%d", state);
1740 epar(state, x, y, h, left);
1741 //if (state & sfText)
1742 y = h;
1743 n->xr = x - 12;
1744 n->yr = y;
1745
1746 addState(state, sfPar);
1747 if (n->container) {
1748 layout(n, n->container, left, right, x, y, w, h, flags, state);
1749 }
1750 y = h;
1751 x = left;
1752 //removeState(state, sfPar | sfText);
1753 break;
1754 case node::pre:
1755 if (x > left) {
1756 x = left;
1757 h += font->height();
1758 y = h;
1759 }
1760 if (n->container) {
1761 Flags fl(this, flags, flags | PRE);
1762 layout(n, n->container, x, right, x, y, w, h, fl, state);
1763 }
1764 y = h;
1765 x = left;
1766 removeState(state, sfPar);
1767 addState(state, sfText);
1768 //msg("set=%d", state);
1769 break;
1770 case node::bold:
1771 if (n->container) {
1772 Flags fl(this, flags, flags | BOLD);
1773 layout(n, n->container, left, right, x, y, w, h, fl, state);
1774 }
1775 break;
1776 case node::italic:
1777 if (n->container) {
1778 Flags fl(this, flags, flags | ITAL);
1779 layout(n, n->container, left, right, x, y, w, h, fl, state);
1780 }
1781 break;
1782 case node::tt:
1783 case node::code:
1784 if (n->container) {
1785 Flags fl(this, flags, flags | MONO);
1786 layout(n, n->container, left, right, x, y, w, h, fl, state);
1787 }
1788 break;
1789 case node::html:
1790 case node::body:
1791 case node::div:
1792 case node::table:
1793 case node::tr:
1794 case node::td:
1795 case node::thead:
1796 case node::tbody:
1797 case node::tfoot:
1798 case node::img:
1799 case node::unknown:
1800 case node::link:
1801 case node::form:
1802 case node::input:
1803 case node::section:
1804 case node::figure:
1805 case node::aside:
1806 case node::footer:
1807 case node::main:
1808 if (n->container) {
1809 layout(n, n->container, left, right, x, y, w, h, flags, state);
1810 }
1811 break;
1812 default:
1813 if (complain) {
1814 tlog("default layout for node type %s", node::to_string(n->type));
1815 }
1816 if (n->container)
1817 layout(n, n->container, left, right, x, y, w, h, flags, state);
1818 break;
1819 }
1820 }
1821 ///puts("}");
1822 }
1823
draw(Graphics & g,node * n1,bool isHref)1824 void HTextView::draw(Graphics &g, node *n1, bool isHref) {
1825 for (node *n = n1; n; n = n->next) {
1826 switch (n->type) {
1827 case node::head:
1828 case node::title:
1829 case node::script:
1830 case node::style:
1831 break;
1832
1833 case node::text:
1834 for (text_node *t = n->wrap; t; t = t->next) {
1835 flagFont(t->fl);
1836 g.setFont(font);
1837 int y = t->y + t->th - font->height() - ty - 5;
1838 g.drawChars(t->text, 0, t->len, t->x - tx, y);
1839 if (isHref) {
1840 g.drawLine(t->x - tx, y + 1, t->x + t->tw - tx, y + 1);
1841 }
1842 }
1843 break;
1844
1845 case node::hrule:
1846 g.setColor(hrFg);
1847 g.drawLine(0 + n->xr - tx, n->yr + 4 - ty, width() - 1 - tx, n->yr + 4 - ty);
1848 g.drawLine(0 + n->xr - tx, n->yr + 5 - ty, width() - 1 - tx, n->yr + 5 - ty);
1849 g.setColor(normalFg);
1850 break;
1851
1852 case node::anchor:
1853 {
1854 attr href;
1855 bool found = n->get_attribute("href", &href);
1856 if (found && href.value.length())
1857 g.setColor(linkFg);
1858 if (n->container)
1859 draw(g, n->container, found || isHref);
1860 if (found && isHref == false)
1861 g.setColor(normalFg);
1862 }
1863 break;
1864
1865 case node::li:
1866 g.fillArc(n->xr - tx, n->yr - ty - 2, 7, 7, 0, 360 * 64);
1867 if (n->container)
1868 draw(g, n->container, isHref);
1869 break;
1870
1871 default:
1872 if (n->container)
1873 draw(g, n->container, isHref);
1874 break;
1875 }
1876 }
1877 }
1878
findNext(node * n1)1879 bool HTextView::findNext(node *n1) {
1880 for (node *n = n1; n; n = n->next) {
1881 if (n->container) {
1882 if (findNext(n->container))
1883 return true;
1884 }
1885 for (text_node *tn = n->wrap; tn; tn = tn->next) {
1886 if (ty < tn->y - tn->th) {
1887 const char* found = strstr(tn->text, search.c_str());
1888 if (found && found < tn->text + tn->len) {
1889 setPos(0, tn->y - tn->th);
1890 return true;
1891 }
1892 }
1893 }
1894 }
1895 return false;
1896 }
1897
startSearch()1898 void HTextView::startSearch() {
1899 while (search.nonempty() && 0 < search[0] && search[0] < ' ') {
1900 search = search.substring(1);
1901 }
1902 if (search.nonempty()) {
1903 if (findNext(fRoot)) {
1904 resetScroll();
1905 }
1906 }
1907 }
1908
handleKey(const XKeyEvent & key)1909 bool HTextView::handleKey(const XKeyEvent &key) {
1910 if (fScrollView->handleScrollKeys(key)) {
1911 return true;
1912 }
1913 if (input && input->visible()) {
1914 return input->handleKey(key);
1915 }
1916
1917 if (key.type == KeyPress) {
1918 KeySym k = keyCodeToKeySym(key.keycode);
1919 int m = KEY_MODMASK(key.state);
1920 if ((m & ControlMask) != 0 && (m & ~ControlMask) == 0) {
1921 if (k == XK_q) {
1922 actionPerformed(actionClose);
1923 return true;
1924 }
1925 if (k == XK_b) {
1926 actionPerformed(actionBrowser);
1927 return true;
1928 }
1929 if (k == XK_r) {
1930 actionPerformed(actionReload);
1931 return true;
1932 }
1933 if (k == XK_i) {
1934 actionPerformed(actionLink[0]);
1935 return true;
1936 }
1937 if (k == XK_m) {
1938 actionPerformed(actionLink[4]);
1939 return true;
1940 }
1941 if (k == XK_t) {
1942 actionPerformed(actionLink[6]);
1943 return true;
1944 }
1945 if (k == XK_w) {
1946 actionPerformed(actionLink[7]);
1947 return true;
1948 }
1949 if (k == XK_v) {
1950 actionPerformed(actionReversed);
1951 return true;
1952 }
1953 if (k == XK_x) {
1954 actionPerformed(actionFreeType);
1955 return true;
1956 }
1957 if (k == XK_f) {
1958 actionPerformed(actionFind);
1959 return true;
1960 }
1961 }
1962 if ((m & xapp->AltMask) != 0 && (m & ~xapp->AltMask) == 0) {
1963 if (k == XK_Left || k == XK_KP_Left) {
1964 actionPerformed(actionLeft);
1965 return true;
1966 }
1967 if (k == XK_Right || k == XK_KP_Right) {
1968 actionPerformed(actionRight);
1969 return true;
1970 }
1971 }
1972 if (notbit(m, ControlMask | xapp->AltMask | ShiftMask)) {
1973 if (k == XK_F3) {
1974 startSearch();
1975 return true;
1976 }
1977 }
1978 }
1979 return false;
1980 }
1981
handleButton(const XButtonEvent & button)1982 void HTextView::handleButton(const XButtonEvent &button) {
1983 if (fVerticalScroll->handleScrollMouse(button) == false)
1984 YWindow::handleButton(button);
1985 }
1986
handleClick(const XButtonEvent & up,int)1987 void HTextView::handleClick(const XButtonEvent &up, int /*count*/) {
1988 if (up.button == Button3) {
1989 menu->popup(nullptr, nullptr, nullptr, up.x_root, up.y_root,
1990 YPopupWindow::pfCanFlipVertical |
1991 YPopupWindow::pfCanFlipHorizontal /*|
1992 YPopupWindow::pfPopupMenu*/);
1993 }
1994 else if (up.button == Button1 && up.x >= int(width()) - 20 && up.y <= 20 - ty) {
1995 menu->popup(this, nullptr, nullptr,
1996 up.x_root - up.x + int(width()) - 1,
1997 up.y_root - up.y,
1998 YPopupWindow::pfFlipHorizontal);
1999 }
2000 else if (up.button == Button1) {
2001 node *anchor = nullptr;
2002 node *n = find_node(fRoot, up.x + tx, up.y + ty, anchor, node::anchor);
2003 if (n && anchor) {
2004 attr href;
2005 if (anchor->get_attribute("href", &href) && href.value) {
2006 listener->activateURL(href.value, true);
2007 }
2008 }
2009 }
2010 }
2011
2012 class FileView: public YDndWindow, public HTListener {
2013 public:
2014 FileView(YApplication *app, int argc, char **argv);
~FileView()2015 ~FileView() {
2016 delete view;
2017 delete scroll;
2018 FontTable::reset();
2019 extern void clearFontCache();
2020 clearFontCache();
2021 }
2022
2023 void activateURL(mstring url, bool relative = false) override;
2024 void openBrowser(mstring url) override;
2025
configure(const YRect2 & r)2026 void configure(const YRect2& r) override {
2027 if (r.resized()) {
2028 scroll->setGeometry(YRect(0, 0, r.width(), r.height()));
2029 }
2030 }
2031
handleClose()2032 void handleClose() override {
2033 if (ignoreClose == false) {
2034 app->exitLoop(0);
2035 }
2036 }
handleExpose(const XExposeEvent & expose)2037 void handleExpose(const XExposeEvent& expose) override {
2038 }
2039
isFocusTraversable()2040 bool isFocusTraversable() override {
2041 return true;
2042 }
2043
2044 private:
2045 bool loadFile(upath path);
2046 bool loadHttp(upath path);
2047 void invalidPath(upath path, const char *reason);
2048 void run(const char* path, const char* arg1 = nullptr,
2049 const char* arg2 = nullptr, const char* arg3 = nullptr);
2050
handleKey(const XKeyEvent & key)2051 bool handleKey(const XKeyEvent &key) override {
2052 return view->handleKey(key);
2053 }
2054
2055 upath fPath;
2056 YApplication *app;
2057
2058 HTextView *view;
2059 YScrollView *scroll;
2060 ref<YPixmap> small_icon;
2061 ref<YPixmap> large_icon;
2062 };
2063
FileView(YApplication * iapp,int argc,char ** argv)2064 FileView::FileView(YApplication *iapp, int argc, char **argv)
2065 : fPath(), app(iapp), view(nullptr), scroll(nullptr)
2066 {
2067 setDND(true);
2068 setStyle(wsNoExpose);
2069 setTitle("IceHelp");
2070
2071 scroll = new YScrollView(this);
2072 view = new HTextView(this, scroll, scroll);
2073 scroll->setView(view);
2074
2075 view->show();
2076 scroll->show();
2077
2078 setSize(ViewerWidth, ViewerHeight);
2079
2080 ref<YIcon> file_icon = YIcon::getIcon("file");
2081 if (file_icon != null) {
2082 Pixmap icons[4];
2083 int count = 0;
2084 if (file_icon->small() != null) {
2085 small_icon = YPixmap::createFromImage(file_icon->small(), xapp->depth());
2086 if (small_icon != null) {
2087 icons[count++] = small_icon->pixmap();
2088 icons[count++] = small_icon->mask();
2089 }
2090 }
2091 if (file_icon->large() != null) {
2092 large_icon = YPixmap::createFromImage(file_icon->large(), xapp->depth());
2093 if (large_icon != null) {
2094 icons[count++] = large_icon->pixmap();
2095 icons[count++] = large_icon->mask();
2096 }
2097 }
2098 if (count > 0) {
2099 extern Atom _XA_WIN_ICONS;
2100 setProperty(_XA_WIN_ICONS, XA_PIXMAP, icons, count);
2101 }
2102 }
2103 setNetPid();
2104
2105 XWMHints wmhints = {};
2106 wmhints.flags = InputHint | StateHint;
2107 wmhints.input = True;
2108 wmhints.initial_state = NormalState;
2109 if (large_icon != null) {
2110 wmhints.icon_pixmap = large_icon->pixmap();
2111 wmhints.icon_mask = large_icon->mask();
2112 wmhints.flags |= IconPixmapHint | IconMaskHint;
2113 }
2114 XSetWMHints(xapp->display(), handle(), &wmhints);
2115
2116 YTextProperty name("icehelp");
2117 YTextProperty icon("icehelp");
2118 XSizeHints size = { PSize, 0, 0,
2119 ViewerWidth, ViewerHeight,
2120 };
2121 XClassHint klas = { const_cast<char *>("icehelp"),
2122 const_cast<char *>("IceWM") };
2123 XSetWMProperties(xapp->display(), handle(),
2124 &name, &icon, argv, argc,
2125 &size, &wmhints, &klas);
2126 }
2127
activateURL(mstring url,bool relative)2128 void FileView::activateURL(mstring url, bool relative) {
2129 if (verbose) {
2130 tlog("activateURL('%s', %s)", url.c_str(),
2131 relative ? "relative" : "not-relative");
2132 }
2133
2134 /*
2135 * Differentiate:
2136 * - has (only) a fragment (#).
2137 * - url is local/remote.
2138 * - if local: absolute or relative to previous path.
2139 */
2140
2141 mstring path, frag;
2142 if (url.splitall('#', &path, &frag) == false ||
2143 path.length() + frag.length() == 0) {
2144 return; // empty
2145 }
2146
2147 if (relative && path.nonempty() && false == upath(path).hasProtocol()) {
2148 if (upath(path).isRelative()) {
2149 int k = fPath.path().lastIndexOf('/');
2150 if (k >= 0) {
2151 path = fPath.path().substring(0, k + 1) + path;
2152 }
2153 }
2154 else if (fPath.hasProtocol()) {
2155 int k = fPath.path().find("://");
2156 if (k > 0) {
2157 int i = fPath.path().substring(k + 3).indexOf('/');
2158 if (i > 0) {
2159 path = fPath.path().substring(0, k + 3 + i) + path;
2160 }
2161 }
2162 }
2163 }
2164 path = path.searchAndReplaceAll("/./", "/");
2165
2166 if (path.length() > 0) {
2167 if (upath(path).hasProtocol()) {
2168 if (path.startsWith("file:///")) {
2169 path = path.substring((int)strlen("file://"));
2170 }
2171 }
2172 if (upath(path).hasProtocol()) {
2173 if (upath(path).isHttp()) {
2174 if (path.count('/') == 2) {
2175 path += "/";
2176 }
2177 if (loadHttp(path) == false) {
2178 return;
2179 }
2180 }
2181 else {
2182 return invalidPath(path, _("Unsupported protocol."));
2183 }
2184 }
2185 else if (loadFile(path) == false) {
2186 return;
2187 }
2188 }
2189 if (frag.length() > 0 && view->contentHeight() > view->height()) {
2190 // search
2191 view->find_fragment(frag);
2192 }
2193 view->repaint();
2194 if (frag.length() > 0) {
2195 path = path + "#" + frag;
2196 }
2197 if (relative && path.charAt(0) == '#') {
2198 path = fPath.path() + path;
2199 }
2200 view->addHistory(path);
2201 fPath = path;
2202 setNetName(path);
2203 }
2204
openBrowser(mstring url)2205 void FileView::openBrowser(mstring url) {
2206 char* env = getenv("BROWSER");
2207 mstring brow(nonempty(env) && !strchr(env, '/')
2208 ? mstring("/usr/bin/") + env : env);
2209 if (brow != null && upath(brow).isExecutable()) {
2210 run(brow, url);
2211 }
2212 else if (upath("/usr/bin/xdg-open").isExecutable()) {
2213 run("/usr/bin/xdg-open", url);
2214 }
2215 else if (upath("/usr/bin/gnome-open").isExecutable()) {
2216 run("/usr/bin/gnome-open", url);
2217 }
2218 else if (upath("/usr/bin/kde-open").isExecutable()) {
2219 run("/usr/bin/kde-open", url);
2220 }
2221 else if (upath("/usr/bin/gio").isExecutable()) {
2222 run("/usr/bin/gio", "open", url);
2223 }
2224 else if (upath("/usr/bin/python3").isExecutable()) {
2225 run("/usr/bin/python3", "-m", "webbrowser", url);
2226 }
2227 else if (upath("/usr/bin/python").isExecutable()) {
2228 run("/usr/bin/python", "-m", "webbrowser", url);
2229 }
2230 else if (upath("/usr/bin/sensible-browser").isExecutable()) {
2231 run("/usr/bin/sensible-browser", url);
2232 }
2233 }
2234
run(const char * path,const char * arg1,const char * arg2,const char * arg3)2235 void FileView::run(const char* path, const char* arg1,
2236 const char* arg2, const char* arg3)
2237 {
2238 char* args[] = {
2239 const_cast<char*>(path),
2240 const_cast<char*>(arg1),
2241 const_cast<char*>(arg2),
2242 const_cast<char*>(arg3),
2243 nullptr
2244 };
2245 switch (fork()) {
2246 case -1:
2247 fail("fork");
2248 break;
2249 case 0:
2250 if (execv(path, args) == -1) {
2251 fail("exec %s", path);
2252 }
2253 break;
2254 default:
2255 break;
2256 }
2257 }
2258
invalidPath(upath path,const char * reason)2259 void FileView::invalidPath(upath path, const char *reason) {
2260 const char *cstr = path.string();
2261 const char *cfmt = _("Invalid path: %s\n");
2262 const char *crea = reason;
2263 tlog(cfmt, cstr);
2264 tlog("%s", crea);
2265
2266 const size_t size = 9 + strlen(cfmt) + strlen(cstr) + strlen(crea);
2267 char *cbuf = (char *)malloc(size);
2268 snprintf(cbuf, size, cfmt, cstr);
2269 strlcat(cbuf, ":\n ", size);
2270 strlcat(cbuf, crea, size);
2271
2272 node *root = new node(node::div);
2273 flist<node> nodes(root);
2274
2275 node *txt = new node(node::text);
2276 txt->txt = cbuf;
2277 nodes.add(txt);
2278
2279 view->setData(root);
2280 view->repaint();
2281 }
2282
loadFile(upath path)2283 bool FileView::loadFile(upath path) {
2284 if (path.fileExists() == false) {
2285 invalidPath(path, _("Path does not refer to a file."));
2286 return false;
2287 }
2288 FILE *fp = fopen(path.string(), "r");
2289 if (fp == nullptr) {
2290 invalidPath(path, _("Failed to open file for reading."));
2291 return false;
2292 }
2293 node *nextsub = nullptr;
2294 node::node_type close_type = node::unknown;
2295 node *root = parse(fp, 0, nullptr, nextsub, close_type);
2296 assert(nextsub == 0);
2297 fclose(fp);
2298 dump_tree(0, root);
2299 view->setData(root);
2300 return true;
2301 }
2302
2303 class temp_file {
2304 private:
2305 FILE *fp;
2306 cbuffer cbuf;
init(const char * tdir)2307 bool init(const char *tdir) {
2308 cbuf = (upath(tdir) + "iceXXXXXX").string();
2309 int fd = mkstemp(cbuf.peek());
2310 if (fd >= 0) fp = fdopen(fd, "w+b");
2311 return fp;
2312 }
2313 temp_file(const temp_file&);
2314 temp_file& operator=(const temp_file&);
2315 public:
temp_file()2316 temp_file() : fp(nullptr) {
2317 const char *tenv = getenv("TMPDIR");
2318 if ((tenv && init(tenv)) || init("/tmp") || init("/var/tmp")) ;
2319 else tlog(_("Failed to create a temporary file"));
2320 }
~temp_file()2321 ~temp_file() {
2322 unlink(cbuf.peek());
2323 if (fp) fclose(fp);
2324 }
operator bool() const2325 operator bool() const { return fp != nullptr; }
fildes() const2326 int fildes() const { return fileno(fp); }
filep() const2327 FILE *filep() const { return fp; }
path() const2328 const char *path() const { return cbuf; }
size() const2329 int size() const {
2330 struct stat b;
2331 return fstat(fildes(), &b) >= 0 ? (int) b.st_size : 0;
2332 }
rewind() const2333 void rewind() const { ::rewind(fp); }
2334 };
2335
2336 class downloader {
2337 private:
2338 mstring curl, wget, gzip, cat;
test(mstring * mst,const mstring & dir,const char * exe)2339 void test(mstring *mst, const mstring& dir, const char *exe) {
2340 if (mst->isEmpty()) {
2341 upath bin = upath(dir) + exe;
2342 if (bin.isExecutable() && bin.fileExists()) {
2343 *mst = bin;
2344 }
2345 }
2346 }
empty() const2347 bool empty() const {
2348 return curl.isEmpty() || wget.isEmpty()
2349 || gzip.isEmpty() || cat.isEmpty();
2350 }
init()2351 void init() {
2352 const char defp[] = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin";
2353 const char *penv = getenv("PATH");
2354 mstring mpth(penv ? penv : defp), mdir;
2355 while (empty() && mpth.splitall(':', &mdir, &mpth)) {
2356 if (mdir.isEmpty())
2357 continue;
2358 test(&curl, mdir, "curl");
2359 test(&wget, mdir, "wget");
2360 test(&gzip, mdir, "gzip");
2361 test(&cat, mdir, "cat");
2362 }
2363 }
2364 downloader(const downloader&);
2365 downloader& operator=(const downloader&);
2366 public:
downloader()2367 downloader() { init(); }
operator bool() const2368 operator bool() const {
2369 return curl.nonempty() || wget.nonempty();
2370 }
downloadTo(const char * remote,const temp_file & local)2371 bool downloadTo(const char *remote, const temp_file& local) {
2372 mstring cmd;
2373 if (curl.nonempty()) {
2374 cmd = curl + " --compressed --location -s -o ";
2375 }
2376 else if (wget.nonempty()) {
2377 cmd = wget + " -q -k -O ";
2378 }
2379 else {
2380 return false;
2381 }
2382 cmd = cmd + "'" + local.path() + "' '" + remote + "'";
2383 if (!command(cmd)) {
2384 return false;
2385 }
2386 local.rewind();
2387 if (is_compressed(local.fildes())) {
2388 if (decompress(local) == false) {
2389 return false;
2390 }
2391 }
2392 local.rewind();
2393 return true;
2394 }
is_safe(const char * url)2395 static bool is_safe(const char *url) {
2396 for (const char *p = url; *p; ++p) {
2397 if (!ASCII::isAlnum(*p) && !strchr(":/.+-_@%?&=", *p)) {
2398 return false;
2399 }
2400 }
2401 return true;
2402 }
is_compressed(int fd)2403 static bool is_compressed(int fd) {
2404 char b[3];
2405 return filereader(fd, false).read_all(BUFNSIZE(b)) >= 2
2406 && b[0] == '\x1F' && b[1] == '\x8B';
2407 }
command(mstring mcmd)2408 static bool command(mstring mcmd) {
2409 const char *cmd = mcmd;
2410 int xit = ::system(cmd);
2411 if (xit) {
2412 tlog(_("Failed to execute system(%s) (%d)"), cmd, xit);
2413 return false;
2414 }
2415 // tlog("Executed system(%s) OK!", cmd);
2416 return true;
2417 }
decompress(const temp_file & local)2418 bool decompress(const temp_file& local) {
2419 if (gzip.nonempty() && cat.nonempty()) {
2420 temp_file temp;
2421 if (temp) {
2422 mstring cmd = gzip + " -d -c <'";
2423 cmd = cmd + local.path() + "' >'";
2424 cmd = cmd + temp.path() + "'";
2425 if (command(cmd)) {
2426 cmd = cat + "<'" + temp.path() + "' >'";
2427 cmd = cmd + local.path() + "'";
2428 if (command(cmd)) {
2429 local.rewind();
2430 return true;
2431 }
2432 }
2433 }
2434 }
2435 tlog(_("Failed to decompress %s"), local.path());
2436 return false;
2437 }
2438 };
2439
loadHttp(upath path)2440 bool FileView::loadHttp(upath path) {
2441 downloader loader;
2442 if (!loader) {
2443 invalidPath(path, _("Could not locate curl or wget in PATH"));
2444 return false;
2445 }
2446 if (!loader.is_safe(path.string())) {
2447 invalidPath(path, _("Unsafe characters in URL"));
2448 return false;
2449 }
2450 temp_file temp;
2451 if (!temp) {
2452 return false;
2453 }
2454 if (loader.downloadTo(path.string(), temp)) {
2455 return loadFile(temp.path());
2456 }
2457 return false;
2458 }
2459
handler(Display * display,XErrorEvent * xev)2460 static int handler(Display *display, XErrorEvent *xev) {
2461 if (true) {
2462 char message[80], req[80], number[80];
2463
2464 snprintf(number, 80, "%d", xev->request_code);
2465 XGetErrorDatabaseText(display, "XRequest",
2466 number, "",
2467 req, sizeof(req));
2468 if (req[0] == 0)
2469 snprintf(req, 80, "[request_code=%d]", xev->request_code);
2470
2471 if (XGetErrorText(display, xev->error_code, message, 80) != Success)
2472 *message = '\0';
2473
2474 tlog("X error %s(0x%lX): %s. #%lu, +%lu, -%lu.",
2475 req, xev->resourceid, message, xev->serial,
2476 NextRequest(display), LastKnownRequestProcessed(display));
2477 }
2478 return 0;
2479 }
2480
print_help()2481 static void print_help()
2482 {
2483 printf(_(
2484 "Usage: %s [OPTIONS] [ FILENAME | URL ]\n\n"
2485 "IceHelp is a very simple HTML browser for the IceWM window manager.\n"
2486 "It can display a HTML document from file, or browse a website.\n"
2487 "It remembers visited pages in a history, which is navigable\n"
2488 "by key bindings and a context menu (right mouse click).\n"
2489 "It neither supports rendering of images nor JavaScript.\n"
2490 "If no file or URL is given it will display the IceWM Manual\n"
2491 "from %s.\n"
2492 "\n"
2493 "Options:\n"
2494 " -d, --display=NAME NAME of the X server to use.\n"
2495 " --sync Synchronize X11 commands.\n"
2496 "\n"
2497 " -B Display the IceWM icewmbg manpage.\n"
2498 " -b, --bugs Display the IceWM bug reports (primitively).\n"
2499 " -f, --faq Display the IceWM FAQ and Howto.\n"
2500 " -g Display the IceWM Github website.\n"
2501 " -i, --icewm Display the IceWM icewm manpage.\n"
2502 " -m, --manual Display the IceWM Manual (default).\n"
2503 " -s Display the IceWM icesound manpage.\n"
2504 " -t, --theme Display the IceWM themes Howto.\n"
2505 " -w, --website Display the IceWM website.\n"
2506 "\n"
2507 " -V, --version Prints version information and exits.\n"
2508 " -h, --help Prints this usage screen and exits.\n"
2509 "\n"
2510 "Environment variables:\n"
2511 " DISPLAY=NAME Name of the X server to use.\n"
2512 "\n"
2513 "To report bugs, support requests, comments please visit:\n"
2514 "%s\n"
2515 "\n"),
2516 ApplicationName,
2517 ICEHELPIDX,
2518 PACKAGE_BUGREPORT);
2519 exit(0);
2520 }
2521
main(int argc,char ** argv)2522 int main(int argc, char **argv) {
2523 YLocale locale;
2524 const char *helpfile(nullptr);
2525 bool nodelete = false, netping = false;
2526
2527 for (char **arg = 1 + argv; arg < argv + argc; ++arg) {
2528 if (**arg == '-') {
2529 if (is_switch(*arg, "b", "bugs"))
2530 helpfile = PACKAGE_BUGREPORT;
2531 else if (is_switch(*arg, "f", "faq"))
2532 helpfile = ICEWM_FAQ;
2533 else if (is_short_switch(*arg, "B"))
2534 helpfile = ICEWMBG_1;
2535 else if (is_short_switch(*arg, "g"))
2536 helpfile = ICEGIT_SITE;
2537 else if (is_switch(*arg, "i", "icewm"))
2538 helpfile = ICEWM_1;
2539 else if (is_switch(*arg, "m", "manual"))
2540 helpfile = ICEHELPIDX;
2541 else if (is_short_switch(*arg, "s"))
2542 helpfile = ICESOUND_1;
2543 else if (is_switch(*arg, "t", "themes"))
2544 helpfile = THEME_HOWTO;
2545 else if (is_switch(*arg, "w", "website"))
2546 helpfile = ICEWM_SITE;
2547 else if (is_help_switch(*arg))
2548 print_help();
2549 else if (is_version_switch(*arg))
2550 print_version_exit(VERSION);
2551 else if (is_copying_switch(*arg))
2552 print_copying_exit();
2553 else if (is_long_switch(*arg, "noclose"))
2554 ignoreClose = true;
2555 else if (is_long_switch(*arg, "nodelete"))
2556 nodelete = true;
2557 else if (is_long_switch(*arg, "noxft"))
2558 noFreeType = true;
2559 else if (is_long_switch(*arg, "reverse"))
2560 reverseVideo = true;
2561 else if (is_long_switch(*arg, "netping"))
2562 netping = true;
2563 else if (is_long_switch(*arg, "verbose"))
2564 verbose = true;
2565 else if (is_long_switch(*arg, "sync")) {
2566 YXApplication::synchronizeX11 = true; }
2567 else if (is_long_switch(*arg, "logevents"))
2568 toggleLogEvents();
2569 else {
2570 char *dummy(nullptr);
2571 if (GetArgument(dummy, "d", "display", arg, argv + argc)) {
2572 /*ignore*/; }
2573 else
2574 warn(_("Ignoring option '%s'"), *arg);
2575 }
2576 }
2577 else {
2578 helpfile = *arg;
2579 }
2580 }
2581 #ifdef PRECON
2582 complain = true;
2583 #elif DEBUG
2584 complain = (debug | verbose);
2585 #else
2586 complain = verbose;
2587 #endif
2588
2589 if (helpfile == nullptr) {
2590 helpfile = ICEHELPIDX;
2591 }
2592
2593 XSetErrorHandler(handler);
2594 YXApplication app(&argc, &argv);
2595 FileView view(&app, argc, argv);
2596 if (nodelete) {
2597 XDeleteProperty(app.display(), view.handle(), _XA_WM_PROTOCOLS);
2598 } else {
2599 extern Atom _XA_NET_WM_PING;
2600 Atom prot[] = {
2601 _XA_WM_DELETE_WINDOW, _XA_WM_TAKE_FOCUS, _XA_NET_WM_PING
2602 };
2603 XSetWMProtocols(app.display(), view.handle(), prot, 2 + netping);
2604 }
2605 view.activateURL(helpfile);
2606 view.show();
2607 app.mainLoop();
2608
2609 return 0;
2610 }
2611
dump_tree(int level,node * n)2612 static void dump_tree(int level, node *n) {
2613 #ifdef DUMP
2614 while (n) {
2615 printf("%*s<%s>\n", level, "", node::to_string(n->type));
2616 if (n->container) {
2617 dump_tree(level + 1, n->container);
2618 printf("%*s</%s>\n", level, "", node::to_string(n->type));
2619 }
2620 n = n->next;
2621 }
2622 #endif
2623 }
2624
2625 #if 0
2626
2627 #define S_LINK "\x1B[04m"
2628 #define E_LINK "\x1B[0m"
2629 #define S_HEAD "\x1B[01;31m"
2630 #define E_HEAD "\x1B[0m"
2631 #define S_BOLD "\x1B[01m"
2632 #define E_BOLD "\x1B[0m"
2633 #define S_ITALIC "\x1B[33m"
2634 #define E_ITALIC "\x1B[0m"
2635 #define S_DEBUG "\x1B[07;31m"
2636 #define E_DEBUG "\x1B[0m"
2637
2638 int pos = 0;
2639 int width = 80;
2640
2641 void clear_line() {
2642 if (pos != 0)
2643 puts("");
2644 pos = 0;
2645 }
2646
2647 void new_line(int count = 1) {
2648 while (count-- > 0)
2649 putchar('\n');
2650 pos = 0;
2651 }
2652
2653 void blank_line(int count = 1) {
2654 clear_line();
2655 new_line(count);
2656 }
2657
2658 void out(char *text) {
2659 printf("%s", text);
2660 pos += strlen(text);
2661 }
2662
2663 void tab(int len) {
2664 pos += len;
2665 while (len-- > 0) putchar(' ');
2666 }
2667
2668 void pre(int left, char *text) {
2669 char *p = text;
2670
2671 clear_line();
2672 while (*p) {
2673 tab(left);
2674 while (*p && *p != '\n') {
2675 putchar(*p);
2676 pos++;
2677 p++;
2678 }
2679 if (*p == '\n')
2680 p++;
2681 new_line();
2682 }
2683 }
2684
2685 void wrap(int left, char *text) {
2686 //putchar('[');
2687 while (*text) {
2688 int chr = 0;
2689 int sp = 0;
2690 int col = pos;
2691 char *p = text;
2692
2693 //printf("[%d:%s]", pos, text);
2694 while (*p && col <= width) {
2695 if (*p == ' ')
2696 sp = chr;
2697 chr++;
2698 col++;
2699 p++;
2700 }
2701 if (col <= width)
2702 sp = chr;
2703 if (sp == 0)
2704 sp = chr;
2705 //printf("(%d)", col);
2706
2707 while (sp > 0) {
2708 putchar(*text);
2709 pos++;
2710 text++;
2711 sp--;
2712 }
2713 if (*text == ' ')
2714 text++;
2715 if (*text) {
2716 //if (pos != width)
2717 puts("");
2718 pos = 0;
2719 tab(left);
2720 }
2721 }
2722 //putchar(']');
2723 }
2724
2725 node *blank(node *n) {
2726 if (n->next && n->next->type == node::text) {
2727 if (n->next->content.text[0] == ' ' &&
2728 n->next->content.text[1] == 0) {
2729 if (n->next->next &&
2730 (n->next->next->type == node::h1 ||
2731 n->next->next->type == node::h2))
2732 return n->next;
2733 blank_line();
2734 return n->next;
2735 }
2736 }
2737 blank_line();
2738 return n;
2739 }
2740
2741 void dump(int flags, int left, node *n, node *up) {
2742 while (n) {
2743 switch (n->type) {
2744 case node::html:
2745 dump(flags, left, n->container, n);
2746 break;
2747 case node::head:
2748 dump(flags, left, n->container, n);
2749 break;
2750 case node::body:
2751 case node::table:
2752 case node::font:
2753 case node::tr:
2754 case node::td:
2755 case node::div:
2756 dump(flags, left, n->container, n);
2757 break;
2758 case node::ul:
2759 case node::ol:
2760 dump(flags, left, n->content.container, n);
2761 if (up && up->type != node::li) {
2762 n = blank(n);
2763 }
2764 break;
2765 case node::li:
2766 clear_line();
2767 tab(left);
2768 out(" * ");
2769 dump(flags, left + 4, n->content.container, n);
2770 break;
2771 case node::bold:
2772 printf(S_BOLD);
2773 dump(flags | BOLD, left, n->content.container, n);
2774 printf(E_BOLD);
2775 break;
2776 case node::italic:
2777 printf(S_ITALIC);
2778 dump(flags | BOLD, left, n->content.container, n);
2779 printf(E_ITALIC);
2780 break;
2781 case node::h1:
2782 case node::h2:
2783 case node::h3:
2784 case node::h4:
2785 case node::h5:
2786 case node::h6:
2787 blank_line();
2788 printf(S_HEAD);
2789 dump(flags, left, n->content.container, n);
2790 printf(E_HEAD);
2791 n = blank(n);
2792 break;
2793 case node::pre:
2794 dump(flags | PRE, left + 4, n->content.container, n);
2795 break;
2796 case node::link:
2797 printf(S_LINK);
2798 dump(flags | LINK, left, n->content.container, n);
2799 printf(E_LINK);
2800 break;
2801 case node::hrule:
2802 clear_line();
2803 for (int i = 0; i < 72; i++)
2804 pos += printf("-");
2805 clear_line();
2806 break;
2807 case node::paragraph:
2808 clear_line();
2809 new_line(1);
2810 break;
2811 case node::line:
2812 new_line(1);
2813 break;
2814 case node::text:
2815 if (flags & PRE)
2816 pre(left, n->content.text);
2817 else {
2818 wrap(left, n->content.text);
2819 }
2820 break;
2821 default:
2822 printf(S_DEBUG "?" E_DEBUG);
2823 }
2824 n = n->next;
2825 }
2826 }
2827
2828 int main(int argc, char **argv) {
2829 FILE *fp = fopen(argv[1], "r");
2830 if (fp == 0)
2831 fp = stdin;
2832 char *e = getenv("COLUMNS");
2833 if (e) width = atoi(e);
2834 if (fp) {
2835 root = parse(fp, 0);
2836 dump(0, 0, root, 0);
2837 clear_line();
2838 }
2839 }
2840 #endif
2841
2842 // vim: set sw=4 ts=4 et:
2843